Merge branch 'main' into aamunger/notebookSymbols

This commit is contained in:
Aaron Munger
2023-09-05 14:31:10 -07:00
committed by GitHub
278 changed files with 5905 additions and 7724 deletions
+41
View File
@@ -196,6 +196,47 @@
]
}
},
{
"files": [
"**/vscode.d.ts"
],
"rules": {
"jsdoc/tag-lines": "off",
"jsdoc/valid-types": "off",
"jsdoc/no-multi-asterisks": [
"warn",
{
"allowWhitespace": true
}
],
"jsdoc/require-jsdoc": [
"warn",
{
"enableFixer": false,
"contexts": [
"TSInterfaceDeclaration",
"TSPropertySignature",
"TSMethodSignature",
"TSDeclareFunction",
"ClassDeclaration",
"MethodDefinition",
"PropertyDeclaration",
"TSEnumDeclaration",
"TSEnumMember",
"ExportNamedDeclaration"
]
}
],
"jsdoc/check-param-names": [
"warn",
{
"enableFixer": false,
"checkDestructured": false
}
],
"jsdoc/require-returns": "warn"
}
},
{
"files": [
"src/**/{common,browser}/**/*.ts"
+5 -1
View File
@@ -256,7 +256,11 @@
"browserLaunchLocation": "workspace",
"presentation": {
"hidden": true,
}
},
// This is read by the vscode-diagnostic-tools extension
"vscode-diagnostic-tools.debuggerScripts": [
"${workspaceFolder}/scripts/hot-reload-injected-script.js"
]
},
{
"type": "node",
-1
View File
@@ -151,5 +151,4 @@
"application.experimental.rendererProfiling": true,
"editor.experimental.asyncTokenization": true,
"editor.experimental.asyncTokenizationVerification": true,
"diffEditor.experimental.useVersion2": true,
}
+2 -2
View File
@@ -1,5 +1,5 @@
disturl "https://electronjs.org/headers"
target "25.7.0"
ms_build_id "23434598"
target "25.8.0"
ms_build_id "23503258"
runtime "electron"
build_from_source "true"
+27 -27
View File
@@ -1,27 +1,27 @@
efbcf77eb1a0783766f9579ffb9f9b68f04fea8cb091eab7ab8484ba0cd13fbf *electron-v25.7.0-darwin-arm64-symbols.zip
76a415165d212a345a5689de83078adc715fc10562bfaa35d7323094780ba683 *electron-v25.7.0-darwin-arm64.zip
07b9049848e877019d1dce71e06713125b605dda8ac5d0b8ab3aa899cf40551d *electron-v25.7.0-darwin-x64-symbols.zip
dea726ae9adc1c36206ce8d20ce32f630bcd684b869e0cb302f97c8bd26616d6 *electron-v25.7.0-darwin-x64.zip
b6c8ba123353984b2d3ffd6ccd52aec2d3238f71611c4c94bab75aa92804eebf *electron-v25.7.0-linux-arm64-symbols.zip
19e1e2c7ea1ab024f069e3dad6a26605e14b2c605e134484196343118fccf925 *electron-v25.7.0-linux-arm64.zip
ba0bbe84ea626c8064809c66487a3b77ad39bcf8b1daa0d9421428f78ad4d665 *electron-v25.7.0-linux-armv7l-symbols.zip
832a68cddb20eb847aca982b89f89e145f50dd483c71c8a705bbb9248fb7c665 *electron-v25.7.0-linux-armv7l.zip
2e616b446112533d3aa69ed1074ab1e0be5400996129aa636273d01462dc9506 *electron-v25.7.0-linux-x64-symbols.zip
002641e8103b77060e23b9c77c51ffb942372d01306210cdc3d32fc6ae5d112b *electron-v25.7.0-linux-x64.zip
162e0f7ca9fc1c17b8d84e9b9eccc65bb0f527a67f6339a19292d798085848e4 *electron-v25.7.0-win32-arm64-pdb.zip
7d98734ffcf10e1d002c30a212dd1f203b1418a295da67410490f83e9ced388c *electron-v25.7.0-win32-arm64-symbols.zip
9777d47f74d129f7c68ebffad640a6a527b83895c173c7d344f80fc9588bad85 *electron-v25.7.0-win32-arm64.zip
c805c6356378dccb21b5725004934534e187bdaf8149a6a457fdd60d243b41e4 *electron-v25.7.0-win32-ia32-pdb.zip
5f1a3b09153cf934f24f3b1853ad1788e7c27c6ddceb80e52fe07e2e69b6bb2b *electron-v25.7.0-win32-ia32-symbols.zip
fdf8e100c3d3cdb75b54ced1ecae96d6206eca08ebb07c5d8f08740e5e703509 *electron-v25.7.0-win32-ia32.zip
aa56314a675351e9457355f2cb0660c62a3be62cc340dad76fd216741064824d *electron-v25.7.0-win32-x64-pdb.zip
25d664dfe0823e1a12269feb6eb3886dba44b2d130b8787c4d58d3d0cbcf1c22 *electron-v25.7.0-win32-x64-symbols.zip
7ddb0b38207fd837cdf4e2b2778c365751315e321b09d346c8bb8476300d0ec0 *electron-v25.7.0-win32-x64.zip
02619733aadb13b6bf21df966e04775506d0d7595a0795003fed45631c4a0af6 *ffmpeg-v25.7.0-darwin-arm64.zip
69a8e2021e48f504021913c15633cbef2b4a7b28656c51cd238acdbf7c94e358 *ffmpeg-v25.7.0-darwin-x64.zip
bd52d57ff97fb56ac01a3482af905d04f0d4e9c13c53858c6d9f99957eca82da *ffmpeg-v25.7.0-linux-arm64.zip
9b3d09177fa1e63e2a6beecfa70aeec30aeb5c1873ff21128a68051c4e23f95d *ffmpeg-v25.7.0-linux-armv7l.zip
edc7b1c9f1a0733f109a2c0375a4e40c5bfe0bf28b7f06dcc76e1ada0aa2f125 *ffmpeg-v25.7.0-linux-x64.zip
7076d4593f2e2e2abf0dc9ad8f6490d72b2fa89710def822f39da4363e49e504 *ffmpeg-v25.7.0-win32-arm64.zip
bd07183c1b6a93586d73c4106ceef0faae77f46763d15d6901d5954c2c5bba1b *ffmpeg-v25.7.0-win32-ia32.zip
b056e71a7c59441c551d5bbc1a8d99f2464a5809a3ba17d41540dc7174cab7b7 *ffmpeg-v25.7.0-win32-x64.zip
88cafda8394985e59d3d84cb4a6692ad04d8e32db9ecd6429e748e41526ddad7 *electron-v25.8.0-darwin-arm64-symbols.zip
6e33d3b8041561722ed41777e055a8c15d3f4e61b67367b2618918bcf0cfea76 *electron-v25.8.0-darwin-arm64.zip
438ac9915e062a239fb6d2595323c4783d2c820efc9cbcf3d2c1253d0e057e83 *electron-v25.8.0-darwin-x64-symbols.zip
798907d2a66bc79202c8213c61e7fd147ae2a8c31c485d814950b11d43bbbba8 *electron-v25.8.0-darwin-x64.zip
3243f3764319cff6c942d9f90a86323c36ec05ec51ef01e782c4e9a7194187e1 *electron-v25.8.0-linux-arm64-symbols.zip
f24f858b76bf8a2e18419f62e0f891712b2fa541089123e9caa8d5cd67fc3276 *electron-v25.8.0-linux-arm64.zip
dc3ff0489a0ebeda56d06b31eeae75dd7321a52bb601069c4475c56462b4814a *electron-v25.8.0-linux-armv7l-symbols.zip
3b7a0c3899f828a5cf30043b73992e90231400b90c1afa700a44f892a55e326b *electron-v25.8.0-linux-armv7l.zip
44803b2487406eca8fff9cec405e9e50bd92a911808dfaaa523b9ef52a0e72d8 *electron-v25.8.0-linux-x64-symbols.zip
d54fb2df0ad7318240220aa26327171ed1e891fb296f3c27c58b8b487c4df8eb *electron-v25.8.0-linux-x64.zip
bf7be6c0c8d0df06f0ce22e16c97aea823415d7f5cbf0ffdadf65d75feaf3cd8 *electron-v25.8.0-win32-arm64-pdb.zip
5d91757660b44bf30907f9c2b52225ade4d127d0fe48dc83dec134cc06c949f0 *electron-v25.8.0-win32-arm64-symbols.zip
d1e6f30a8d8c7aed28d08ddf915d79de6b16b3a0a7c84c45fd3cc0d47f2b7f53 *electron-v25.8.0-win32-arm64.zip
e389fef61c14ea0eefad91a9725aa0afd4dbdc982f7b30aba97bd9c2871c2061 *electron-v25.8.0-win32-ia32-pdb.zip
374d6c8897f97fab04e990ecf928e05f643ae33801546bf7d39bf4045b9d8b52 *electron-v25.8.0-win32-ia32-symbols.zip
73fc3382202b70dcaf7928f09a791662de82c701b8f403ed72cc5aa9b1401593 *electron-v25.8.0-win32-ia32.zip
010d248bd2e77585e1fa977e58b016659566de5a91c1e6845c85a7e6e1851bb9 *electron-v25.8.0-win32-x64-pdb.zip
72adb74fd92edff35c177c3c5d96765f230bc7adb8af11b30d5122b9e54c26e1 *electron-v25.8.0-win32-x64-symbols.zip
0051d0f241aedc6cdab4751c60f48758936122796f06c9e3033c7710a531686c *electron-v25.8.0-win32-x64.zip
2956915642c45eb0099228368d0af50e891e4c10014fa4d3d3bcfb135fbb89a7 *ffmpeg-v25.8.0-darwin-arm64.zip
099ee69d44f8ac3802cdd612895f279f7adb043a5b9c9d123479b0f96514a44c *ffmpeg-v25.8.0-darwin-x64.zip
bd52d57ff97fb56ac01a3482af905d04f0d4e9c13c53858c6d9f99957eca82da *ffmpeg-v25.8.0-linux-arm64.zip
9b3d09177fa1e63e2a6beecfa70aeec30aeb5c1873ff21128a68051c4e23f95d *ffmpeg-v25.8.0-linux-armv7l.zip
edc7b1c9f1a0733f109a2c0375a4e40c5bfe0bf28b7f06dcc76e1ada0aa2f125 *ffmpeg-v25.8.0-linux-x64.zip
a58e9480dab981ff973749e9d1e08936b2dd63a4b7f9523c030b1833387a4eb5 *ffmpeg-v25.8.0-win32-arm64.zip
6866b23a4d561c0322aeb7690aae646718c54398739946e352bf80d0dd721bfd *ffmpeg-v25.8.0-win32-ia32.zip
7b906df4ad6252881cf1e58619285b624f74d593379fbc6728e238b852d6abad *ffmpeg-v25.8.0-win32-x64.zip
-4
View File
@@ -89,7 +89,6 @@ declare namespace monaco {
}
declare namespace monaco.editor {
#include(vs/editor/browser/widget/diffNavigator): IDiffNavigator
#includeAll(vs/editor/standalone/browser/standaloneEditor;languages.Token=>Token):
#include(vs/editor/standalone/common/standaloneTheme): BuiltinTheme, IStandaloneThemeData, IColors
#include(vs/editor/common/languages/supports/tokenization): ITokenThemeRule
@@ -108,9 +107,6 @@ export interface ICommandHandler {
#include(vs/editor/common/core/wordHelper): IWordAtPosition
#includeAll(vs/editor/common/model): IScrollEvent
#include(vs/editor/common/diff/legacyLinesDiffComputer): IChange, ICharChange, ILineChange
#include(vs/editor/common/diff/documentDiffProvider): IDocumentDiffProvider, IDocumentDiffProviderOptions, IDocumentDiff
#include(vs/editor/common/core/lineRange): LineRange
#include(vs/editor/common/diff/linesDiffComputer): LineRangeMapping, RangeMapping, MovedText, SimpleLineRangeMapping
#include(vs/editor/common/core/dimension): IDimension
#includeAll(vs/editor/common/editorCommon): IScrollEvent
#includeAll(vs/editor/common/textModelEvents):
+2 -2
View File
@@ -528,12 +528,12 @@
"git": {
"name": "electron",
"repositoryUrl": "https://github.com/electron/electron",
"commitHash": "f818ec3295c9688585e3cfea532ccc5b705746bb"
"commitHash": "84d7f7f071ae11637d4a41b95536410293672750"
}
},
"isOnlyProductionDependency": true,
"license": "MIT",
"version": "25.7.0"
"version": "25.8.0"
},
{
"component": {
+2 -2
View File
@@ -323,8 +323,8 @@ fn format(level: Level, prefix: &str, message: &str, use_colors: bool) -> String
}
pub fn emit(level: Level, prefix: &str, message: &str) {
let line = format(level, prefix, message, true);
if level == Level::Trace {
let line = format(level, prefix, message, *COLORS_ENABLED);
if level == Level::Trace && *COLORS_ENABLED {
print!("\x1b[2m{}\x1b[0m", line);
} else {
print!("{}", line);
+3
View File
@@ -1021,6 +1021,9 @@ where
p.current_dir(cwd);
}
#[cfg(target_os = "windows")]
p.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
let mut p = p.spawn().map_err(CodeError::ProcessSpawnFailed)?;
let futs = FuturesUnordered::new();
+10 -4
View File
@@ -14,7 +14,13 @@ The Git extension exposes an API, reachable by any other extension.
2. Include `git.d.ts` in your extension's compilation.
3. Get a hold of the API with the following snippet:
```ts
const gitExtension = vscode.extensions.getExtension<GitExtension>('vscode.git').exports;
const git = gitExtension.getAPI(1);
```
```ts
const gitExtension = vscode.extensions.getExtension<GitExtension>('vscode.git').exports;
const git = gitExtension.getAPI(1);
```
**Note:** To ensure that the `vscode.git` extension is activated before your extension, add `extensionDependencies` ([docs](https://code.visualstudio.com/api/references/extension-manifest)) into the `package.json` of your extension:
```json
"extensionDependencies": [
"vscode.git"
]
```
+1 -1
View File
@@ -3012,7 +3012,7 @@
"jschardet": "3.0.0",
"picomatch": "2.3.1",
"vscode-uri": "^2.0.0",
"which": "3.0.1"
"which": "4.0.0"
},
"devDependencies": {
"@types/byline": "4.2.31",
+2
View File
@@ -129,6 +129,8 @@ export interface LogOptions {
/** Max number of log entries to retrieve. If not specified, the default is 32. */
readonly maxEntries?: number;
readonly path?: string;
/** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */
readonly range?: string;
}
export interface CommitOptions {
+8 -1
View File
@@ -1022,7 +1022,14 @@ export class Repository {
async log(options?: LogOptions): Promise<Commit[]> {
const maxEntries = options?.maxEntries ?? 32;
const args = ['log', `-n${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z', '--'];
const args = ['log', `-n${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z'];
if (options?.range) {
args.push(options.range);
}
args.push('--');
if (options?.path) {
args.push(options.path);
}
+9 -9
View File
@@ -516,10 +516,10 @@ is-core-module@^2.13.0:
dependencies:
has "^1.0.3"
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
isexe@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d"
integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==
jschardet@3.0.0:
version "3.0.0"
@@ -679,12 +679,12 @@ vscode-uri@^2.0.0:
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.0.0.tgz#2df704222f72b8a71ff266ba0830ed6c51ac1542"
integrity sha512-lWXWofDSYD8r/TIyu64MdwB4FaSirQ608PP/TzUyslyOeHGwQ0eTHUZeJrK1ILOmwUHaJtV693m2JoUYroUDpw==
which@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/which/-/which-3.0.1.tgz#89f1cd0c23f629a8105ffe69b8172791c87b4be1"
integrity sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==
which@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/which/-/which-4.0.0.tgz#cd60b5e74503a3fbcfbf6cd6b4138a8bae644c1a"
integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==
dependencies:
isexe "^2.0.0"
isexe "^3.1.1"
yallist@^4.0.0:
version "4.0.0"
@@ -200,7 +200,9 @@ const allFlows: IFlow[] = [
// other flows that work well.
supportsGitHubEnterpriseServer: false,
supportsHostedGitHubEnterprise: true,
supportsRemoteExtensionHost: true,
// Opening a port on the remote side can't be open in the browser on
// the client side so this flow won't work in remote extension hosts
supportsRemoteExtensionHost: false,
// Web worker can't open a port to listen for the redirect
supportsWebWorkerExtensionHost: false,
// exchanging a code for a token requires a client secret
@@ -8,7 +8,7 @@ export type JSONLanguageStatus = { schemas: string[] };
import {
workspace, window, languages, commands, ExtensionContext, extensions, Uri, ColorInformation,
Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange,
ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n, TextEditorOptions
ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n
} from 'vscode';
import {
LanguageClientOptions, RequestType, NotificationType, FormattingOptions as LSPFormattingOptions,
@@ -102,7 +102,6 @@ export type JSONSchemaSettings = {
export namespace SettingIds {
export const enableFormatter = 'json.format.enable';
export const enableKeepLines = 'json.format.keepLines';
export const enableSortOnSave = 'json.sortOnSave.enable';
export const enableValidation = 'json.validate.enable';
export const enableSchemaDownload = 'json.schemaDownload.enable';
export const maxItemsComputed = 'json.maxItemsComputed';
@@ -171,15 +170,6 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
window.showInformationMessage(l10n.t('JSON schema cache cleared.'));
}));
toDispose.push(workspace.onWillSaveTextDocument(event => {
const sortOnSave = workspace.getConfiguration().get<boolean>(SettingIds.enableSortOnSave);
const document = event.document;
if (sortOnSave && (document.languageId === 'json' || document.languageId === 'jsonc')) {
const documentOptions = getOptionsForDocument(document);
const textEditsPromise = getSortTextEdits(document, documentOptions?.tabSize, documentOptions?.insertSpaces);
event.waitUntil(textEditsPromise);
}
}));
toDispose.push(commands.registerCommand('json.sort', async () => {
@@ -643,12 +633,3 @@ function updateMarkdownString(h: MarkdownString): MarkdownString {
function isSchemaResolveError(d: Diagnostic) {
return d.code === /* SchemaResolveError */ 0x300;
}
function getOptionsForDocument(document: TextDocument): TextEditorOptions | undefined {
for (const editor of window.visibleTextEditors) {
if (editor.document.uri.toString() === document.uri.toString()) {
return editor.options;
}
}
return;
}
@@ -91,12 +91,6 @@
"default": false,
"description": "%json.format.keepLines.desc%"
},
"json.sortOnSave.enable": {
"type": "boolean",
"scope": "window",
"default": false,
"description": "%json.sortOnSave.enable.desc%"
},
"json.trace.server": {
"type": "string",
"scope": "window",
@@ -8,7 +8,6 @@
"json.schemas.schema.desc": "The schema definition for the given URL. The schema only needs to be provided to avoid accesses to the schema URL.",
"json.format.enable.desc": "Enable/disable default JSON formatter",
"json.format.keepLines.desc" : "Keep all existing new lines when formatting.",
"json.sortOnSave.enable.desc": "Enable/disable default sorting on save",
"json.validate.enable.desc": "Enable/disable JSON validation.",
"json.tracing.desc": "Traces the communication between VS Code and the JSON language server.",
"json.colorDecorators.enable.desc": "Enables or disables color decorators",
@@ -6,7 +6,7 @@
import {
Connection,
TextDocuments, InitializeParams, InitializeResult, NotificationType, RequestType,
DocumentRangeFormattingRequest, Disposable, ServerCapabilities, TextDocumentSyncKind, TextEdit, DocumentFormattingRequest, TextDocumentIdentifier, FormattingOptions, Diagnostic
DocumentRangeFormattingRequest, Disposable, ServerCapabilities, TextDocumentSyncKind, TextEdit, DocumentFormattingRequest, TextDocumentIdentifier, FormattingOptions, Diagnostic, CodeAction, CodeActionKind
} from 'vscode-languageserver';
import { runSafe, runSafeAsync } from './utils/runner';
@@ -14,6 +14,7 @@ import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnostics
import { TextDocument, JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration, ClientCapabilities, Range, Position, SortOptions } from 'vscode-json-languageservice';
import { getLanguageModelCache } from './languageModelCache';
import { Utils, URI } from 'vscode-uri';
import * as l10n from '@vscode/l10n';
type ISchemaAssociations = Record<string, string[]>;
@@ -188,7 +189,8 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
documentSelector: null,
interFileDependencies: false,
workspaceDiagnostics: false
}
},
codeActionProvider: true
};
return { capabilities };
@@ -424,6 +426,21 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
}, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`, token);
});
connection.onCodeAction((codeActionParams, token) => {
return runSafeAsync(runtime, async () => {
const document = documents.get(codeActionParams.textDocument.uri);
if (document) {
const sortCodeAction = CodeAction.create('Sort JSON', CodeActionKind.Source.concat('.sort', '.json'));
sortCodeAction.command = {
command: 'json.sort',
title: l10n.t('Sort JSON')
};
return [sortCodeAction];
}
return [];
}, [], `Error while computing code actions for ${codeActionParams.textDocument.uri}`, token);
});
function onFormat(textDocument: TextDocumentIdentifier, range: Range | undefined, options: FormattingOptions): TextEdit[] {
options.keepLines = keepLinesEnabled;
+5 -1
View File
@@ -81,7 +81,11 @@
"meta.embedded.block.typescriptreact": "typescriptreact",
"meta.embedded.block.csharp": "csharp",
"meta.embedded.block.fsharp": "fsharp"
}
},
"unbalancedBracketScopes": [
"markup.underline.link.markdown",
"punctuation.definition.list.begin.markdown"
]
}
],
"snippets": [
+11 -11
View File
@@ -231,8 +231,8 @@ class TunnelProvider implements vscode.TunnelProvider {
];
this.logger.log('info', '[forwarding] starting CLI');
const process = spawn(cliPath, args, { stdio: 'pipe' });
this.state = { state: State.Starting, process };
const child = spawn(cliPath, args, { stdio: 'pipe', env: { ...process.env, NO_COLOR: '1' } });
this.state = { state: State.Starting, process: child };
const progressP = new DeferredPromise<void>();
vscode.window.withProgress(
@@ -248,29 +248,29 @@ class TunnelProvider implements vscode.TunnelProvider {
);
let lastPortFormat: string | undefined;
process.on('exit', status => {
child.on('exit', status => {
const msg = `[forwarding] exited with code ${status}`;
this.logger.log('info', msg);
progressP.complete(); // make sure to clear progress on unexpected exit
if (this.isInStateWithProcess(process)) {
if (this.isInStateWithProcess(child)) {
this.state = { state: State.Error, error: msg };
}
});
process.on('error', err => {
child.on('error', err => {
this.logger.log('error', `[forwarding] ${err}`);
progressP.complete(); // make sure to clear progress on unexpected exit
if (this.isInStateWithProcess(process)) {
if (this.isInStateWithProcess(child)) {
this.state = { state: State.Error, error: String(err) };
}
});
process.stdout
child.stdout
.pipe(splitNewLines())
.on('data', line => this.logger.log('info', `[forwarding] ${line}`))
.resume();
process.stderr
child.stderr
.pipe(splitNewLines())
.on('data', line => {
try {
@@ -278,7 +278,7 @@ class TunnelProvider implements vscode.TunnelProvider {
if (l.port_format && l.port_format !== lastPortFormat) {
this.state = {
state: State.Active,
portFormat: l.port_format, process,
portFormat: l.port_format, process: child,
cleanupTimeout: 'cleanupTimeout' in this.state ? this.state.cleanupTimeout : undefined,
};
progressP.complete();
@@ -290,8 +290,8 @@ class TunnelProvider implements vscode.TunnelProvider {
.resume();
await new Promise((resolve, reject) => {
process.on('spawn', resolve);
process.on('error', reject);
child.on('spawn', resolve);
child.on('error', reject);
});
}
}
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.82.0",
"distro": "021e674d5265eb9125cfc0282c3a9a6091f4982d",
"version": "1.83.0",
"distro": "46e7bb69af9f06de037c3d7e8f61de4a679f9ef1",
"author": {
"name": "Microsoft Corporation"
},
@@ -107,7 +107,7 @@
"yazl": "^2.4.3"
},
"devDependencies": {
"@playwright/test": "^1.34.3",
"@playwright/test": "^1.37.1",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.62",
"@types/cookie": "^0.3.3",
@@ -150,10 +150,10 @@
"cssnano": "^4.1.11",
"debounce": "^1.0.0",
"deemon": "^1.8.0",
"electron": "25.7.0",
"electron": "25.8.0",
"eslint": "8.36.0",
"eslint-plugin-header": "3.1.1",
"eslint-plugin-jsdoc": "^39.3.2",
"eslint-plugin-jsdoc": "^46.5.0",
"eslint-plugin-local": "^1.0.0",
"event-stream": "3.3.4",
"fancy-log": "^1.3.3",
@@ -212,7 +212,7 @@
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tsec": "0.2.7",
"typescript": "^5.3.0-dev.20230824",
"typescript": "^5.3.0-dev.20230905",
"typescript-formatter": "7.1.0",
"underscore": "^1.12.1",
"util": "^0.12.4",
+22
View File
@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
type RunFunction = ((debugSession: IDebugSession) => IDisposable) | ((debugSession: IDebugSession) => Promise<IDisposable>);
interface IDebugSession {
name: string;
eval(expression: string): Promise<void>;
evalJs<T extends any[]>(bodyFn: (...args: T) => void, ...args: T): Promise<void>;
}
interface IDisposable {
dispose(): void;
}
interface GlobalThisAddition extends globalThis {
$hotReload_applyNewExports?(oldExports: Record<string, unknown>): AcceptNewExportsFn | undefined;
}
type AcceptNewExportsFn = (newExports: Record<string, unknown>) => boolean;
+267
View File
@@ -0,0 +1,267 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
/// <reference path='debugger-scripts-api.d.ts' />
const path = require('path');
const fsPromise = require('fs/promises');
const parcelWatcher = require('@parcel/watcher');
// This file is loaded by the vscode-diagnostic-tools extension and injected into the debugger.
/** @type {RunFunction} */
module.exports.run = async function (debugSession) {
const watcher = await DirWatcher.watchRecursively(path.join(__dirname, '../out/'));
const sub = watcher.onDidChange(changes => {
const supportedChanges = changes.filter(c => c.path.endsWith('.js') || c.path.endsWith('.css'));
debugSession.evalJs(function (changes, debugSessionName) {
// This function is stringified and injected into the debuggee.
/** @type {{ count: number; originalWindowTitle: any; timeout: any; shouldReload: boolean }} */
const hotReloadData = globalThis.$hotReloadData || (globalThis.$hotReloadData = { count: 0, messageHideTimeout: undefined, shouldReload: false });
/**
* @param {string} path
* @param {string} newSrc
*/
function handleChange(path, newSrc) {
const relativePath = path.replace(/\\/g, '/').split('/out/')[1];
if (relativePath.endsWith('.css')) {
handleCssChange(relativePath);
} else if (relativePath.endsWith('.js')) {
handleJsChange(relativePath, newSrc);
}
}
/**
* @param {string} relativePath
*/
function handleCssChange(relativePath) {
if (typeof document === 'undefined') {
return;
}
const styleSheet = (/** @type {HTMLLinkElement[]} */ ([...document.querySelectorAll(`link[rel='stylesheet']`)]))
.find(l => new URL(l.href, document.location.href).pathname.endsWith(relativePath));
if (styleSheet) {
setMessage(`reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`);
console.log(debugSessionName, 'css reloaded', relativePath);
styleSheet.href = styleSheet.href.replace(/\?.*/, '') + '?' + Date.now();
} else {
setMessage(`could not reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`);
console.log(debugSessionName, 'ignoring css change, as stylesheet is not loaded', relativePath);
}
}
/**
* @param {string} relativePath
* @param {string} newSrc
*/
function handleJsChange(relativePath, newSrc) {
const moduleIdStr = trimEnd(relativePath, '.js');
/** @type {any} */
const requireFn = globalThis.require;
const moduleManager = requireFn.moduleManager;
if (!moduleManager) {
console.log(debugSessionName, 'ignoring js change, as moduleManager is not available', relativePath);
return;
}
const moduleId = moduleManager._moduleIdProvider.getModuleId(moduleIdStr);
const oldModule = moduleManager._modules2[moduleId];
if (!oldModule) {
console.log(debugSessionName, 'ignoring js change, as module is not loaded', relativePath);
return;
}
// Check if we can reload
const g = /** @type {GlobalThisAddition} */ (globalThis);
// A frozen copy of the previous exports
const oldExports = Object.freeze({ ...oldModule.exports });
const reloadFn = g.$hotReload_applyNewExports?.(oldExports);
if (!reloadFn) {
console.log(debugSessionName, 'ignoring js change, as module does not support hot-reload', relativePath);
hotReloadData.shouldReload = true;
setMessage(`hot reload not supported for ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`);
return;
}
const newScript = new Function('define', newSrc); // CodeQL [SM01632] This code is only executed during development. It is required for the hot-reload functionality.
newScript(/* define */ function (deps, callback) {
// Evaluating the new code was successful.
// Redefine the module
delete moduleManager._modules2[moduleId];
moduleManager.defineModule(moduleIdStr, deps, callback);
const newModule = moduleManager._modules2[moduleId];
// Patch the exports of the old module, so that modules using the old module get the new exports
Object.assign(oldModule.exports, newModule.exports);
// We override the exports so that future reloads still patch the initial exports.
newModule.exports = oldModule.exports;
const successful = reloadFn(newModule.exports);
if (!successful) {
hotReloadData.shouldReload = true;
setMessage(`hot reload failed ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`);
console.log(debugSessionName, 'hot reload was not successful', relativePath);
return;
}
console.log(debugSessionName, 'hot reloaded', moduleIdStr);
setMessage(`successfully reloaded ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`);
});
}
/**
* @param {string} message
*/
function setMessage(message) {
const domElem = /** @type {HTMLDivElement | undefined} */ (document.querySelector('.titlebar-center .window-title'));
if (!domElem) { return; }
if (!hotReloadData.timeout) {
hotReloadData.originalWindowTitle = domElem.innerText;
} else {
clearTimeout(hotReloadData.timeout);
}
if (hotReloadData.shouldReload) {
message += ' (manual reload required)';
}
domElem.innerText = message;
hotReloadData.timeout = setTimeout(() => {
hotReloadData.timeout = undefined;
// If wanted, we can restore the previous title message
// domElem.replaceChildren(hotReloadData.originalWindowTitle);
}, 5000);
}
/**
* @param {string} path
* @returns {string}
*/
function formatPath(path) {
const parts = path.split('/');
parts.reverse();
let result = parts[0];
parts.shift();
for (const p of parts) {
if (result.length + p.length > 40) {
break;
}
result = p + '/' + result;
if (result.length > 20) {
break;
}
}
return result;
}
function trimEnd(str, suffix) {
if (str.endsWith(suffix)) {
return str.substring(0, str.length - suffix.length);
}
return str;
}
for (const change of changes) {
handleChange(change.path, change.newContent);
}
}, supportedChanges, debugSession.name.substring(0, 25));
});
return {
dispose() {
sub.dispose();
watcher.dispose();
}
};
};
class DirWatcher {
/**
*
* @param {string} dir
* @returns {Promise<DirWatcher>}
*/
static async watchRecursively(dir) {
/** @type {((changes: { path: string, newContent: string }[]) => void)[]} */
const listeners = [];
/** @type {Map<string, string> } */
const fileContents = new Map();
/** @type {Map<string, { path: string, newContent: string }>} */
const changes = new Map();
/** @type {(handler: (changes: { path: string, newContent: string }[]) => void) => IDisposable} */
const event = (handler) => {
listeners.push(handler);
return {
dispose: () => {
const idx = listeners.indexOf(handler);
if (idx >= 0) {
listeners.splice(idx, 1);
}
}
};
};
const r = parcelWatcher.subscribe(dir, async (err, events) => {
for (const e of events) {
if (e.type === 'update') {
const newContent = await fsPromise.readFile(e.path, 'utf8');
if (fileContents.get(e.path) !== newContent) {
fileContents.set(e.path, newContent);
changes.set(e.path, { path: e.path, newContent });
}
}
}
if (changes.size > 0) {
debounce(() => {
const uniqueChanges = Array.from(changes.values());
changes.clear();
listeners.forEach(l => l(uniqueChanges));
})();
}
});
const result = await r;
return new DirWatcher(event, () => result.unsubscribe());
}
/**
* @param {(handler: (changes: { path: string, newContent: string }[]) => void) => IDisposable} onDidChange
* @param {() => void} unsub
*/
constructor(onDidChange, unsub) {
this.onDidChange = onDidChange;
this.unsub = unsub;
}
dispose() {
this.unsub();
}
}
/**
* Debounce function calls
* @param {() => void} fn
* @param {number} delay
*/
function debounce(fn, delay = 50) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
+5 -2
View File
@@ -920,8 +920,11 @@ class FocusTracker extends Disposable implements IFocusTracker {
this._register(addDisposableListener(element, EventType.FOCUS, onFocus, true));
this._register(addDisposableListener(element, EventType.BLUR, onBlur, true));
this._register(addDisposableListener(element, EventType.FOCUS_IN, () => this._refreshStateHandler()));
this._register(addDisposableListener(element, EventType.FOCUS_OUT, () => this._refreshStateHandler()));
if (element instanceof HTMLElement) {
this._register(addDisposableListener(element, EventType.FOCUS_IN, () => this._refreshStateHandler()));
this._register(addDisposableListener(element, EventType.FOCUS_OUT, () => this._refreshStateHandler()));
}
}
refreshState() {
+1 -2
View File
@@ -8,8 +8,7 @@ import { equals, tail2 as tail } from 'vs/base/common/arrays';
import { Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import 'vs/css!./gridview';
import { Box, GridView, IGridViewOptions, IGridViewStyles, IView as IGridViewView, IViewSize, orthogonal, Sizing as GridViewSizing } from './gridview';
import type { GridLocation } from 'vs/base/browser/ui/grid/gridview';
import { Box, GridView, IGridViewOptions, IGridViewStyles, IView as IGridViewView, IViewSize, orthogonal, Sizing as GridViewSizing, GridLocation } from './gridview';
import type { SplitView, AutoSizing as SplitViewAutoSizing } from 'vs/base/browser/ui/splitview/splitview';
export { IViewSize, LayoutPriority, Orientation, orthogonal } from './gridview';
+2 -2
View File
@@ -403,11 +403,11 @@ export class ListView<T> implements IListView<T> {
this.disposables.add(Gesture.addTarget(this.rowsContainer));
this.scrollable = new Scrollable({
this.scrollable = this.disposables.add(new Scrollable({
forceIntegerValues: true,
smoothScrollDuration: (options.smoothScrolling ?? false) ? 125 : 0,
scheduleAtNextAnimationFrame: cb => scheduleAtNextAnimationFrame(cb)
});
}));
this.scrollableElement = this.disposables.add(new SmoothScrollableElement(this.rowsContainer, {
alwaysConsumeMouseWheel: options.alwaysConsumeMouseWheel ?? DefaultOptions.alwaysConsumeMouseWheel,
horizontal: ScrollbarVisibility.Auto,
+80 -66
View File
@@ -293,12 +293,15 @@ class KeyboardController<T> implements IDisposable {
private readonly disposables = new DisposableStore();
private readonly multipleSelectionDisposables = new DisposableStore();
private multipleSelectionSupport: boolean | undefined;
@memoize
private get onKeyDown(): Event.IChainableEvent<StandardKeyboardEvent> {
return this.disposables.add(Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event)
.filter(e => !isInputElement(e.target as HTMLElement))
.map(e => new StandardKeyboardEvent(e)));
private get onKeyDown(): Event<StandardKeyboardEvent> {
return Event.chain(
this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>
$.filter(e => !isInputElement(e.target as HTMLElement))
.map(e => new StandardKeyboardEvent(e))
);
}
constructor(
@@ -306,25 +309,32 @@ class KeyboardController<T> implements IDisposable {
private view: IListView<T>,
options: IListOptions<T>
) {
this.onKeyDown.filter(e => e.keyCode === KeyCode.Enter).on(this.onEnter, this, this.disposables);
this.onKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(this.onUpArrow, this, this.disposables);
this.onKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(this.onDownArrow, this, this.disposables);
this.onKeyDown.filter(e => e.keyCode === KeyCode.PageUp).on(this.onPageUpArrow, this, this.disposables);
this.onKeyDown.filter(e => e.keyCode === KeyCode.PageDown).on(this.onPageDownArrow, this, this.disposables);
this.onKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(this.onEscape, this, this.disposables);
if (options.multipleSelectionSupport !== false) {
this.onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === KeyCode.KeyA).on(this.onCtrlA, this, this.multipleSelectionDisposables);
}
this.multipleSelectionSupport = options.multipleSelectionSupport;
this.disposables.add(this.onKeyDown(e => {
switch (e.keyCode) {
case KeyCode.Enter:
return this.onEnter(e);
case KeyCode.UpArrow:
return this.onUpArrow(e);
case KeyCode.DownArrow:
return this.onDownArrow(e);
case KeyCode.PageUp:
return this.onPageUpArrow(e);
case KeyCode.PageDown:
return this.onPageDownArrow(e);
case KeyCode.Escape:
return this.onEscape(e);
case KeyCode.KeyA:
if (this.multipleSelectionSupport && (platform.isMacintosh ? e.metaKey : e.ctrlKey)) {
this.onCtrlA(e);
}
}
}));
}
updateOptions(optionsUpdate: IListOptionsUpdate): void {
if (optionsUpdate.multipleSelectionSupport !== undefined) {
this.multipleSelectionDisposables.clear();
if (optionsUpdate.multipleSelectionSupport) {
this.onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === KeyCode.KeyA).on(this.onCtrlA, this, this.multipleSelectionDisposables);
}
this.multipleSelectionSupport = optionsUpdate.multipleSelectionSupport;
}
}
@@ -464,15 +474,15 @@ class TypeNavigationController<T> implements IDisposable {
let typing = false;
const onChar = this.enabledDisposables.add(Event.chain(this.enabledDisposables.add(new DomEmitter(this.view.domNode, 'keydown')).event))
.filter(e => !isInputElement(e.target as HTMLElement))
.filter(() => this.mode === TypeNavigationMode.Automatic || this.triggered)
.map(event => new StandardKeyboardEvent(event))
.filter(e => typing || this.keyboardNavigationEventFilter(e))
.filter(e => this.delegate.mightProducePrintableCharacter(e))
.forEach(e => EventHelper.stop(e, true))
.map(event => event.browserEvent.key)
.event;
const onChar = Event.chain(this.enabledDisposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>
$.filter(e => !isInputElement(e.target as HTMLElement))
.filter(() => this.mode === TypeNavigationMode.Automatic || this.triggered)
.map(event => new StandardKeyboardEvent(event))
.filter(e => typing || this.keyboardNavigationEventFilter(e))
.filter(e => this.delegate.mightProducePrintableCharacter(e))
.forEach(e => EventHelper.stop(e, true))
.map(event => event.browserEvent.key)
);
const onClear = Event.debounce<string, null>(onChar, () => null, 800, undefined, undefined, undefined, this.enabledDisposables);
const onInput = Event.reduce<string | null, string | null>(Event.any(onChar, onClear), (r, i) => i === null ? null : ((r || '') + i), undefined, this.enabledDisposables);
@@ -529,14 +539,21 @@ class TypeNavigationController<T> implements IDisposable {
if (this.list.options.typeNavigationEnabled) {
if (typeof labelStr !== 'undefined') {
const prefix = matchesPrefix(word, labelStr);
// If prefix is found, focus and return early
if (matchesPrefix(word, labelStr)) {
this.previouslyFocused = start;
this.list.setFocus([index]);
this.list.reveal(index);
return;
}
const fuzzy = matchesFuzzy2(word, labelStr);
if (fuzzy) {
const fuzzyScore = fuzzy[0].end - fuzzy[0].start;
// ensures that when fuzzy matching, doesn't clash with prefix matching (1 input vs 1+ should be prefix and fuzzy respecitvely). Also makes sure that exact matches are prioritized.
if (prefix || (fuzzyScore > 1 && fuzzy.length === 1)) {
if (fuzzyScore > 1 && fuzzy.length === 1) {
this.previouslyFocused = start;
this.list.setFocus([index]);
this.list.reveal(index);
@@ -544,13 +561,11 @@ class TypeNavigationController<T> implements IDisposable {
}
}
}
} else {
if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) {
this.previouslyFocused = start;
this.list.setFocus([index]);
this.list.reveal(index);
return;
}
} else if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) {
this.previouslyFocused = start;
this.list.setFocus([index]);
this.list.reveal(index);
return;
}
}
}
@@ -570,12 +585,14 @@ class DOMFocusController<T> implements IDisposable {
private list: List<T>,
private view: IListView<T>
) {
const onKeyDown = this.disposables.add(Event.chain(this.disposables.add(new DomEmitter(view.domNode, 'keydown')).event))
const onKeyDown = Event.chain(this.disposables.add(new DomEmitter(view.domNode, 'keydown')).event, $ => $
.filter(e => !isInputElement(e.target as HTMLElement))
.map(e => new StandardKeyboardEvent(e));
.map(e => new StandardKeyboardEvent(e))
);
onKeyDown.filter(e => e.keyCode === KeyCode.Tab && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey)
.on(this.onTab, this, this.disposables);
const onTab = Event.chain(onKeyDown, $ => $.filter(e => e.keyCode === KeyCode.Tab && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey));
onTab(this.onTab, this, this.disposables);
}
private onTab(e: StandardKeyboardEvent): void {
@@ -993,7 +1010,6 @@ export interface IListOptions<T> extends IListOptionsUpdate {
readonly keyboardNavigationLabelProvider?: IKeyboardNavigationLabelProvider<T>;
readonly keyboardNavigationDelegate?: IKeyboardNavigationDelegate;
readonly keyboardSupport?: boolean;
readonly keyboardNavigationEnabled?: boolean;
readonly multipleSelectionController?: IMultipleSelectionController<T>;
readonly styleController?: (suffix: string) => IStyleController;
readonly accessibilityProvider?: IListAccessibilityProvider<T>;
@@ -1348,31 +1364,29 @@ export class List<T> implements ISpliceable<T>, IDisposable {
@memoize get onContextMenu(): Event<IListContextMenuEvent<T>> {
let didJustPressContextMenuKey = false;
const fromKeyDown = this.disposables.add(Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event))
.map(e => new StandardKeyboardEvent(e))
.filter(e => didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
.map(e => EventHelper.stop(e, true))
.filter(() => false)
.event as Event<any>;
const fromKeyDown: Event<any> = Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event, $ =>
$.map(e => new StandardKeyboardEvent(e))
.filter(e => didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
.map(e => EventHelper.stop(e, true))
.filter(() => false));
const fromKeyUp = this.disposables.add(Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event))
.forEach(() => didJustPressContextMenuKey = false)
.map(e => new StandardKeyboardEvent(e))
.filter(e => e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
.map(e => EventHelper.stop(e, true))
.map(({ browserEvent }) => {
const focus = this.getFocus();
const index = focus.length ? focus[0] : undefined;
const element = typeof index !== 'undefined' ? this.view.element(index) : undefined;
const anchor = typeof index !== 'undefined' ? this.view.domElement(index) as HTMLElement : this.view.domNode;
return { index, element, anchor, browserEvent };
})
.event;
const fromKeyUp = Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event, $ =>
$.forEach(() => didJustPressContextMenuKey = false)
.map(e => new StandardKeyboardEvent(e))
.filter(e => e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
.map(e => EventHelper.stop(e, true))
.map(({ browserEvent }) => {
const focus = this.getFocus();
const index = focus.length ? focus[0] : undefined;
const element = typeof index !== 'undefined' ? this.view.element(index) : undefined;
const anchor = typeof index !== 'undefined' ? this.view.domElement(index) as HTMLElement : this.view.domNode;
return { index, element, anchor, browserEvent };
}));
const fromMouse = this.disposables.add(Event.chain(this.view.onContextMenu))
.filter(_ => !didJustPressContextMenuKey)
.map(({ element, index, browserEvent }) => ({ element, index, anchor: new StandardMouseEvent(browserEvent), browserEvent }))
.event;
const fromMouse = Event.chain(this.view.onContextMenu, $ =>
$.filter(_ => !didJustPressContextMenuKey)
.map(({ element, index, browserEvent }) => ({ element, index, anchor: new StandardMouseEvent(browserEvent), browserEvent }))
);
return Event.any<IListContextMenuEvent<T>>(fromKeyDown, fromKeyUp, fromMouse);
}
@@ -626,14 +626,14 @@ export class DomScrollableElement extends AbstractScrollableElement {
super(element, options, scrollable);
this._register(scrollable);
this._element = element;
this.onScroll((e) => {
this._register(this.onScroll((e) => {
if (e.scrollTopChanged) {
this._element.scrollTop = e.scrollTop;
}
if (e.scrollLeftChanged) {
this._element.scrollLeft = e.scrollLeft;
}
});
}));
this.scanDomNode();
}
@@ -752,20 +752,21 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
// SetUp list keyboard controller - control navigation, disabled items, focus
const onKeyDown = this._register(new DomEmitter(this.selectDropDownListContainer, 'keydown'));
const onSelectDropDownKeyDown = Event.chain(onKeyDown.event)
.filter(() => this.selectList.length > 0)
.map(e => new StandardKeyboardEvent(e));
const onSelectDropDownKeyDown = Event.chain(onKeyDown.event, $ =>
$.filter(() => this.selectList.length > 0)
.map(e => new StandardKeyboardEvent(e))
);
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Enter).on(e => this.onEnter(e), this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Tab).on(e => this.onEnter(e), this)); // Tab should behave the same as enter, #79339
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(e => this.onEscape(e), this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(e => this.onUpArrow(e), this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(e => this.onDownArrow(e), this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.PageDown).on(this.onPageDown, this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.PageUp).on(this.onPageUp, this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Home).on(this.onHome, this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.End).on(this.onEnd, this));
this._register(onSelectDropDownKeyDown.filter(e => (e.keyCode >= KeyCode.Digit0 && e.keyCode <= KeyCode.KeyZ) || (e.keyCode >= KeyCode.Semicolon && e.keyCode <= KeyCode.NumpadDivide)).on(this.onCharacter, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === KeyCode.Enter))(this.onEnter, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === KeyCode.Tab))(this.onEnter, this)); // Tab should behave the same as enter, #79339
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === KeyCode.Escape))(this.onEscape, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === KeyCode.UpArrow))(this.onUpArrow, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === KeyCode.DownArrow))(this.onDownArrow, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === KeyCode.PageDown))(this.onPageDown, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === KeyCode.PageUp))(this.onPageUp, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === KeyCode.Home))(this.onHome, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === KeyCode.End))(this.onEnd, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => (e.keyCode >= KeyCode.Digit0 && e.keyCode <= KeyCode.KeyZ) || (e.keyCode >= KeyCode.Semicolon && e.keyCode <= KeyCode.NumpadDivide)))(this.onCharacter, this));
// SetUp list mouse controller - control navigation, disabled items, focus
this._register(dom.addDisposableListener(this.selectList.getHTMLElement(), dom.EventType.POINTER_UP, e => this.onPointerUp(e)));
@@ -212,6 +212,7 @@ export class ToolBar extends Disposable {
override dispose(): void {
this.clear();
this.disposables.dispose();
super.dispose();
}
}
+18 -18
View File
@@ -19,7 +19,7 @@ import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTr
import { ICollapseStateChangeEvent, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeEvent, ITreeFilter, ITreeModel, ITreeModelSpliceEvent, ITreeMouseEvent, ITreeNavigator, ITreeNode, ITreeRenderer, TreeDragOverBubble, TreeError, TreeFilterResult, TreeMouseEventTarget, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
import { Action } from 'vs/base/common/actions';
import { distinct, equals, firstOrDefault, range } from 'vs/base/common/arrays';
import { disposableTimeout, timeout } from 'vs/base/common/async';
import { Delayer, disposableTimeout, timeout } from 'vs/base/common/async';
import { Codicon } from 'vs/base/common/codicons';
import { ThemeIcon } from 'vs/base/common/themables';
import { SetMap } from 'vs/base/common/collections';
@@ -814,9 +814,7 @@ class FindWidget<T, TFilterData> extends Disposable {
this.mode = mode;
const emitter = this._register(new DomEmitter(this.findInput.inputBox.inputElement, 'keydown'));
const onKeyDown = this._register(Event.chain(emitter.event))
.map(e => new StandardKeyboardEvent(e))
.event;
const onKeyDown = Event.chain(emitter.event, $ => $.map(e => new StandardKeyboardEvent(e)));
this._register(onKeyDown((e): any => {
// Using equals() so we reserve modified keys for future use
@@ -886,9 +884,7 @@ class FindWidget<T, TFilterData> extends Disposable {
}));
}));
const onGrabKeyDown = this._register(Event.chain(this._register(new DomEmitter(this.elements.grab, 'keydown')).event))
.map(e => new StandardKeyboardEvent(e))
.event;
const onGrabKeyDown = Event.chain(this._register(new DomEmitter(this.elements.grab, 'keydown')).event, $ => $.map(e => new StandardKeyboardEvent(e)));
this._register(onGrabKeyDown((e): any => {
let right: number | undefined;
@@ -1624,9 +1620,10 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
// We debounce it with 0 delay since these events may fire in the same stack and we only
// want to run this once. It also doesn't matter if it runs on the next tick since it's only
// a nice to have UI feature.
onDidChangeActiveNodes.input = Event.chain(Event.any<any>(onDidModelSplice, this.focus.onDidChange, this.selection.onDidChange))
.debounce(() => null, 0)
.map(() => {
const activeNodesEmitter = this.disposables.add(new Emitter<ITreeNode<T, TFilterData>[]>());
const activeNodesDebounce = this.disposables.add(new Delayer(0));
this.disposables.add(Event.any<any>(onDidModelSplice, this.focus.onDidChange, this.selection.onDidChange)(() => {
activeNodesDebounce.trigger(() => {
const set = new Set<ITreeNode<T, TFilterData>>();
for (const node of this.focus.getNodes()) {
@@ -1637,17 +1634,20 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
set.add(node);
}
return [...set.values()];
}).event;
activeNodesEmitter.fire([...set.values()]);
});
}));
onDidChangeActiveNodes.input = activeNodesEmitter.event;
if (_options.keyboardSupport !== false) {
const onKeyDown = Event.chain(this.view.onKeyDown)
.filter(e => !isInputElement(e.target as HTMLElement))
.map(e => new StandardKeyboardEvent(e));
const onKeyDown = Event.chain(this.view.onKeyDown, $ =>
$.filter(e => !isInputElement(e.target as HTMLElement))
.map(e => new StandardKeyboardEvent(e))
);
onKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow).on(this.onLeftArrow, this, this.disposables);
onKeyDown.filter(e => e.keyCode === KeyCode.RightArrow).on(this.onRightArrow, this, this.disposables);
onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables);
Event.chain(onKeyDown, $ => $.filter(e => e.keyCode === KeyCode.LeftArrow))(this.onLeftArrow, this, this.disposables);
Event.chain(onKeyDown, $ => $.filter(e => e.keyCode === KeyCode.RightArrow))(this.onRightArrow, this, this.disposables);
Event.chain(onKeyDown, $ => $.filter(e => e.keyCode === KeyCode.Space))(this.onSpace, this, this.disposables);
}
if ((_options.findWidgetEnabled ?? true) && _options.keyboardNavigationLabelProvider && _options.contextViewProvider) {
+37 -115
View File
@@ -6,6 +6,7 @@
import { CancellationToken } from 'vs/base/common/cancellation';
import { CancellationError } from 'vs/base/common/errors';
import { ISplice } from 'vs/base/common/sequence';
import { findFirstIdxMonotonousOrArrLen } from './arraysFind';
/**
* Returns the last element of an array.
@@ -106,27 +107,6 @@ export function binarySearch2(length: number, compareToKey: (index: number) => n
return -(low + 1);
}
/**
* Takes a sorted array and a function p. The array is sorted in such a way that all elements where p(x) is false
* are located before all elements where p(x) is true.
* @returns the least x for which p(x) is true or array.length if no element fullfills the given function.
*/
export function findFirstInSorted<T>(array: ReadonlyArray<T>, p: (x: T) => boolean): number {
let low = 0, high = array.length;
if (high === 0) {
return 0; // no children
}
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (p(array[mid])) {
high = mid;
} else {
low = mid + 1;
}
}
return low;
}
type Compare<T> = (a: T, b: T) => number;
@@ -177,6 +157,36 @@ export function groupBy<T>(data: ReadonlyArray<T>, compare: (a: T, b: T) => numb
return result;
}
/**
* Splits the given items into a list of (non-empty) groups.
* `shouldBeGrouped` is used to decide if two consecutive items should be in the same group.
* The order of the items is preserved.
*/
export function* groupAdjacentBy<T>(items: Iterable<T>, shouldBeGrouped: (item1: T, item2: T) => boolean): Iterable<T[]> {
let currentGroup: T[] | undefined;
let last: T | undefined;
for (const item of items) {
if (last !== undefined && shouldBeGrouped(last, item)) {
currentGroup!.push(item);
} else {
if (currentGroup) {
yield currentGroup;
}
currentGroup = [item];
}
last = item;
}
if (currentGroup) {
yield currentGroup;
}
}
export function forEachAdjacent<T>(arr: T[], f: (item1: T | undefined, item2: T | undefined) => void): void {
for (let i = 0; i <= arr.length; i++) {
f(i === 0 ? undefined : arr[i - 1], i === arr.length ? undefined : arr[i]);
}
}
interface IMutableSplice<T> extends ISplice<T> {
readonly toInsert: T[];
deleteCount: number;
@@ -315,7 +325,7 @@ function topStep<T>(array: ReadonlyArray<T>, compare: (a: T, b: T) => number, re
const element = array[i];
if (compare(element, result[n - 1]) < 0) {
result.pop();
const j = findFirstInSorted(result, e => compare(element, e) < 0);
const j = findFirstIdxMonotonousOrArrLen(result, e => compare(element, e) < 0);
result.splice(j, 0, element);
}
}
@@ -397,26 +407,6 @@ export function uniqueFilter<T, R>(keyFn: (t: T) => R): (t: T) => boolean {
};
}
export function findLast<T>(arr: readonly T[], predicate: (item: T) => boolean): T | undefined {
const idx = findLastIndex(arr, predicate);
if (idx === -1) {
return undefined;
}
return arr[idx];
}
export function findLastIndex<T>(array: ReadonlyArray<T>, fn: (item: T) => boolean): number {
for (let i = array.length - 1; i >= 0; i--) {
const element = array[i];
if (fn(element)) {
return i;
}
}
return -1;
}
export function firstOrDefault<T, NotFound = T>(array: ReadonlyArray<T>, notFoundValue: NotFound): T | NotFound;
export function firstOrDefault<T>(array: ReadonlyArray<T>): T | undefined;
export function firstOrDefault<T, NotFound = T>(array: ReadonlyArray<T>, notFoundValue?: NotFound): T | NotFound | undefined {
@@ -592,20 +582,6 @@ export function getRandomElement<T>(arr: T[]): T | undefined {
return arr[Math.floor(Math.random() * arr.length)];
}
/**
* Returns the first mapped value of the array which is not undefined.
*/
export function mapFind<T, R>(array: Iterable<T>, mapFn: (value: T) => R | undefined): R | undefined {
for (const value of array) {
const mapped = mapFn(value);
if (mapped !== undefined) {
return mapped;
}
}
return undefined;
}
/**
* Insert the new items in the array.
* @param array The original array.
@@ -637,7 +613,11 @@ export function insertInto<T>(array: T[], start: number, newItems: T[]): void {
*/
export function splice<T>(array: T[], start: number, deleteCount: number, newItems: T[]): T[] {
const index = getActualStartIndex(array, start);
const result = array.splice(index, deleteCount);
let result = array.splice(index, deleteCount);
if (result === undefined) {
// see https://bugs.webkit.org/show_bug.cgi?id=261140
result = [];
}
insertInto(array, index, newItems);
return result;
}
@@ -717,64 +697,6 @@ export function reverseOrder<TItem>(comparator: Comparator<TItem>): Comparator<T
return (a, b) => -comparator(a, b);
}
/**
* Returns the first item that is equal to or greater than every other item.
*/
export function findMaxBy<T>(items: readonly T[], comparator: Comparator<T>): T | undefined {
if (items.length === 0) {
return undefined;
}
let max = items[0];
for (let i = 1; i < items.length; i++) {
const item = items[i];
if (comparator(item, max) > 0) {
max = item;
}
}
return max;
}
/**
* Returns the last item that is equal to or greater than every other item.
*/
export function findLastMaxBy<T>(items: readonly T[], comparator: Comparator<T>): T | undefined {
if (items.length === 0) {
return undefined;
}
let max = items[0];
for (let i = 1; i < items.length; i++) {
const item = items[i];
if (comparator(item, max) >= 0) {
max = item;
}
}
return max;
}
/**
* Returns the first item that is equal to or less than every other item.
*/
export function findMinBy<T>(items: readonly T[], comparator: Comparator<T>): T | undefined {
return findMaxBy(items, (a, b) => -comparator(a, b));
}
export function findMaxIdxBy<T>(items: readonly T[], comparator: Comparator<T>): number {
if (items.length === 0) {
return -1;
}
let maxIdx = 0;
for (let i = 1; i < items.length; i++) {
const item = items[i];
if (comparator(item, items[maxIdx]) > 0) {
maxIdx = i;
}
}
return maxIdx;
}
export class ArrayQueue<T> {
private firstIdx = 0;
private lastIdx = this.items.length - 1;
+202
View File
@@ -0,0 +1,202 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Comparator } from './arrays';
export function findLast<T>(array: readonly T[], predicate: (item: T) => boolean): T | undefined {
const idx = findLastIdx(array, predicate);
if (idx === -1) {
return undefined;
}
return array[idx];
}
export function findLastIdx<T>(array: readonly T[], predicate: (item: T) => boolean): number {
for (let i = array.length - 1; i >= 0; i--) {
const element = array[i];
if (predicate(element)) {
return i;
}
}
return -1;
}
/**
* Finds the last item where predicate is true using binary search.
* `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
*
* @returns `undefined` if no item matches, otherwise the last item that matches the predicate.
*/
export function findLastMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean): T | undefined {
const idx = findLastIdxMonotonous(array, predicate);
return idx === -1 ? undefined : array[idx];
}
/**
* Finds the last item where predicate is true using binary search.
* `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
*
* @returns `startIdx - 1` if predicate is false for all items, otherwise the index of the last item that matches the predicate.
*/
export function findLastIdxMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
let i = startIdx;
let j = endIdxEx;
while (i < j) {
const k = Math.floor((i + j) / 2);
if (predicate(array[k])) {
i = k + 1;
} else {
j = k;
}
}
return i - 1;
}
/**
* Finds the first item where predicate is true using binary search.
* `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[false, ..., false, true, ..., true]`!
*
* @returns `undefined` if no item matches, otherwise the first item that matches the predicate.
*/
export function findFirstMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean): T | undefined {
const idx = findFirstIdxMonotonousOrArrLen(array, predicate);
return idx === array.length ? undefined : array[idx];
}
/**
* Finds the first item where predicate is true using binary search.
* `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[false, ..., false, true, ..., true]`!
*
* @returns `endIdxEx` if predicate is false for all items, otherwise the index of the first item that matches the predicate.
*/
export function findFirstIdxMonotonousOrArrLen<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
let i = startIdx;
let j = endIdxEx;
while (i < j) {
const k = Math.floor((i + j) / 2);
if (predicate(array[k])) {
j = k;
} else {
i = k + 1;
}
}
return i;
}
export function findFirstIdxMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
const idx = findFirstIdxMonotonousOrArrLen(array, predicate, startIdx, endIdxEx);
return idx === array.length ? -1 : idx;
}
/**
* Use this when
* * You have a sorted array
* * You query this array with a monotonous predicate to find the last item that has a certain property.
* * You query this array multiple times with monotonous predicates that get weaker and weaker.
*/
export class MonotonousArray<T> {
public static assertInvariants = false;
private _findLastMonotonousLastIdx = 0;
private _prevFindLastPredicate: ((item: T) => boolean) | undefined;
constructor(private readonly _array: readonly T[]) {
}
/**
* The predicate must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
* For subsequent calls, current predicate must be weaker than (or equal to) the previous predicate, i.e. more entries must be `true`.
*/
findLastMonotonous(predicate: (item: T) => boolean): T | undefined {
if (MonotonousArray.assertInvariants) {
if (this._prevFindLastPredicate) {
for (const item of this._array) {
if (this._prevFindLastPredicate(item) && !predicate(item)) {
throw new Error('MonotonousArray: current predicate must be weaker than (or equal to) the previous predicate.');
}
}
}
this._prevFindLastPredicate = predicate;
}
const idx = findLastIdxMonotonous(this._array, predicate, this._findLastMonotonousLastIdx);
this._findLastMonotonousLastIdx = idx + 1;
return idx === -1 ? undefined : this._array[idx];
}
}
/**
* Returns the first item that is equal to or greater than every other item.
*/
export function findFirstMaxBy<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
if (array.length === 0) {
return undefined;
}
let max = array[0];
for (let i = 1; i < array.length; i++) {
const item = array[i];
if (comparator(item, max) > 0) {
max = item;
}
}
return max;
}
/**
* Returns the last item that is equal to or greater than every other item.
*/
export function findLastMaxBy<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
if (array.length === 0) {
return undefined;
}
let max = array[0];
for (let i = 1; i < array.length; i++) {
const item = array[i];
if (comparator(item, max) >= 0) {
max = item;
}
}
return max;
}
/**
* Returns the first item that is equal to or less than every other item.
*/
export function findFirstMinBy<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
return findFirstMaxBy(array, (a, b) => -comparator(a, b));
}
export function findMaxIdxBy<T>(array: readonly T[], comparator: Comparator<T>): number {
if (array.length === 0) {
return -1;
}
let maxIdx = 0;
for (let i = 1; i < array.length; i++) {
const item = array[i];
if (comparator(item, array[maxIdx]) > 0) {
maxIdx = i;
}
}
return maxIdx;
}
/**
* Returns the first mapped value of the array which is not undefined.
*/
export function mapFindFirst<T, R>(items: Iterable<T>, mapFn: (value: T) => R | undefined): R | undefined {
for (const value of items) {
const mapped = mapFn(value);
if (mapped !== undefined) {
return mapped;
}
}
return undefined;
}
+4 -10
View File
@@ -201,16 +201,10 @@ export function illegalState(name?: string): Error {
}
}
export function readonly(name?: string): Error {
return name
? new Error(`readonly property '${name} cannot be changed'`)
: new Error('readonly property cannot be changed');
}
export function disposed(what: string): Error {
const result = new Error(`${what} has been disposed`);
result.name = 'DISPOSED';
return result;
export class ReadonlyError extends TypeError {
constructor(name?: string) {
super(name ? `${name} is read-only and cannot be changed` : 'Cannot change read-only property');
}
}
export function getErrorMessage(err: any): string {
+98 -78
View File
@@ -165,7 +165,10 @@ export namespace Event {
export function any<T>(...events: Event<T>[]): Event<T>;
export function any(...events: Event<any>[]): Event<void>;
export function any<T>(...events: Event<T>[]): Event<T> {
return (listener, thisArgs = null, disposables?) => combinedDisposable(...events.map(event => event(e => listener.call(thisArgs, e), null, disposables)));
return (listener, thisArgs = null, disposables?) => {
const disposable = combinedDisposable(...events.map(event => event(e => listener.call(thisArgs, e))));
return addAndReturnDisposable(disposable, disposables);
};
}
/**
@@ -205,6 +208,19 @@ export namespace Event {
return emitter.event;
}
/**
* Adds the IDisposable to the store if it's set, and returns it. Useful to
* Event function implementation.
*/
function addAndReturnDisposable<T extends IDisposable>(d: T, store: DisposableStore | IDisposable[] | undefined): T {
if (store instanceof Array) {
store.push(d);
} else if (store) {
store.add(d);
}
return d;
}
/**
* Given an event, creates a new emitter that event that will debounce events based on {@link delay} and give an
* array event object of all events that fired.
@@ -421,79 +437,6 @@ export namespace Event {
return emitter.event;
}
export interface IChainableEvent<T> extends IDisposable {
event: Event<T>;
map<O>(fn: (i: T) => O): IChainableEvent<O>;
forEach(fn: (i: T) => void): IChainableEvent<T>;
filter(fn: (e: T) => boolean): IChainableEvent<T>;
filter<R>(fn: (e: T | R) => e is R): IChainableEvent<R>;
reduce<R>(merge: (last: R | undefined, event: T) => R, initial?: R): IChainableEvent<R>;
latch(): IChainableEvent<T>;
debounce(merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number): IChainableEvent<T>;
debounce<R>(merge: (last: R | undefined, event: T) => R, delay?: number, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number): IChainableEvent<R>;
on(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;
once(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable;
}
class ChainableEvent<T> implements IChainableEvent<T> {
private readonly disposables = new DisposableStore();
constructor(readonly event: Event<T>) { }
/** @see {@link Event.map} */
map<O>(fn: (i: T) => O): IChainableEvent<O> {
return new ChainableEvent(map(this.event, fn, this.disposables));
}
/** @see {@link Event.forEach} */
forEach(fn: (i: T) => void): IChainableEvent<T> {
return new ChainableEvent(forEach(this.event, fn, this.disposables));
}
/** @see {@link Event.filter} */
filter(fn: (e: T) => boolean): IChainableEvent<T>;
filter<R>(fn: (e: T | R) => e is R): IChainableEvent<R>;
filter(fn: (e: T) => boolean): IChainableEvent<T> {
return new ChainableEvent(filter(this.event, fn, this.disposables));
}
/** @see {@link Event.reduce} */
reduce<R>(merge: (last: R | undefined, event: T) => R, initial?: R): IChainableEvent<R> {
return new ChainableEvent(reduce(this.event, merge, initial, this.disposables));
}
/** @see {@link Event.reduce} */
latch(): IChainableEvent<T> {
return new ChainableEvent(latch(this.event, undefined, this.disposables));
}
/** @see {@link Event.debounce} */
debounce(merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number): IChainableEvent<T>;
debounce<R>(merge: (last: R | undefined, event: T) => R, delay?: number, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number): IChainableEvent<R>;
debounce<R>(merge: (last: R | undefined, event: T) => R, delay: number = 100, leading = false, flushOnListenerRemove = false, leakWarningThreshold?: number): IChainableEvent<R> {
return new ChainableEvent(debounce(this.event, merge, delay, leading, flushOnListenerRemove, leakWarningThreshold, this.disposables));
}
/**
* Attach a listener to the event.
*/
on(listener: (e: T) => any, thisArgs: any, disposables: IDisposable[] | DisposableStore) {
return this.event(listener, thisArgs, disposables);
}
/** @see {@link Event.once} */
once(listener: (e: T) => any, thisArgs: any, disposables: IDisposable[]) {
return once(this.event)(listener, thisArgs, disposables);
}
dispose() {
this.disposables.dispose();
}
}
/**
* Wraps the event in an {@link IChainableEvent}, allowing a more functional programming style.
*
@@ -506,14 +449,91 @@ export namespace Event {
* ).event;
*
* // Using chain
* const onEnterPressChain = Event.chain(onKeyPress.event)
* const onEnterPressChain = Event.chain(onKeyPress.event, $ => $
* .map(e => new StandardKeyboardEvent(e))
* .filter(e => e.keyCode === KeyCode.Enter)
* .event;
* );
* ```
*/
export function chain<T>(event: Event<T>): IChainableEvent<T> {
return new ChainableEvent(event);
export function chain<T, R>(event: Event<T>, sythensize: ($: IChainableSythensis<T>) => IChainableSythensis<R>): Event<R> {
const fn: Event<R> = (listener, thisArgs, disposables) => {
const cs = sythensize(new ChainableSynthesis()) as ChainableSynthesis;
return event(function (value) {
const result = cs.evaluate(value);
if (result !== HaltChainable) {
listener.call(thisArgs, result);
}
}, undefined, disposables);
};
return fn;
}
const HaltChainable = Symbol('HaltChainable');
class ChainableSynthesis implements IChainableSythensis<any> {
private readonly steps: ((input: any) => any)[] = [];
map<O>(fn: (i: any) => O): this {
this.steps.push(fn);
return this;
}
forEach(fn: (i: any) => void): this {
this.steps.push(v => {
fn(v);
return v;
});
return this;
}
filter(fn: (e: any) => boolean): this {
this.steps.push(v => fn(v) ? v : HaltChainable);
return this;
}
reduce<R>(merge: (last: R | undefined, event: any) => R, initial?: R | undefined): this {
let last = initial;
this.steps.push(v => {
last = merge(last, v);
return last;
});
return this;
}
latch(equals: (a: any, b: any) => boolean = (a, b) => a === b): ChainableSynthesis {
let firstCall = true;
let cache: any;
this.steps.push(value => {
const shouldEmit = firstCall || !equals(value, cache);
firstCall = false;
cache = value;
return shouldEmit ? value : HaltChainable;
});
return this;
}
public evaluate(value: any) {
for (const step of this.steps) {
value = step(value);
if (value === HaltChainable) {
break;
}
}
return value;
}
}
export interface IChainableSythensis<T> {
map<O>(fn: (i: T) => O): IChainableSythensis<O>;
forEach(fn: (i: T) => void): IChainableSythensis<T>;
filter(fn: (e: T) => boolean): IChainableSythensis<T>;
filter<R>(fn: (e: T | R) => e is R): IChainableSythensis<R>;
reduce<R>(merge: (last: R, event: T) => R, initial: R): IChainableSythensis<R>;
reduce<R>(merge: (last: R | undefined, event: T) => R): IChainableSythensis<R>;
latch(equals?: (a: T, b: T) => boolean): IChainableSythensis<T>;
}
export interface NodeEventEmitter {
+59
View File
@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import { env } from 'vs/base/common/process';
export function isHotReloadEnabled(): boolean {
return !!env['VSCODE_DEV'];
}
export function registerHotReloadHandler(handler: HotReloadHandler): IDisposable {
if (!isHotReloadEnabled()) {
return { dispose() { } };
} else {
const handlers = registerGlobalHotReloadHandler();
handlers.add(handler);
return {
dispose() { handlers.delete(handler); }
};
}
}
/**
* Takes the old exports of the module to reload and returns a function to apply the new exports.
* If `undefined` is returned, this handler is not able to handle the module.
*
* If no handler can apply the new exports, the module will not be reloaded.
*/
export type HotReloadHandler = (oldExports: Record<string, unknown>) => AcceptNewExportsHandler | undefined;
export type AcceptNewExportsHandler = (newExports: Record<string, unknown>) => boolean;
function registerGlobalHotReloadHandler() {
if (!hotReloadHandlers) {
hotReloadHandlers = new Set();
}
const g = globalThis as unknown as GlobalThisAddition;
if (!g.$hotReload_applyNewExports) {
g.$hotReload_applyNewExports = oldExports => {
for (const h of hotReloadHandlers!) {
const result = h(oldExports);
if (result) { return result; }
}
return undefined;
};
}
return hotReloadHandlers;
}
let hotReloadHandlers: Set<(oldExports: Record<string, unknown>) => AcceptNewExportsFn | undefined> | undefined = undefined;
interface GlobalThisAddition {
$hotReload_applyNewExports?(oldExports: Record<string, unknown>): AcceptNewExportsFn | undefined;
}
type AcceptNewExportsFn = (newExports: Record<string, unknown>) => boolean;
+60
View File
@@ -664,3 +664,63 @@ export class CounterSet<T> {
return this.map.has(value);
}
}
/**
* A map that allows access both by keys and values.
* **NOTE**: values need to be unique.
*/
export class BidirectionalMap<K, V> {
private readonly _m1 = new Map<K, V>();
private readonly _m2 = new Map<V, K>();
constructor(entries?: readonly (readonly [K, V])[]) {
if (entries) {
for (const [key, value] of entries) {
this.set(key, value);
}
}
}
clear(): void {
this._m1.clear();
this._m2.clear();
}
set(key: K, value: V): void {
this._m1.set(key, value);
this._m2.set(value, key);
}
get(key: K): V | undefined {
return this._m1.get(key);
}
getKey(value: V): K | undefined {
return this._m2.get(value);
}
delete(key: K): boolean {
const value = this._m1.get(key);
if (value === undefined) {
return false;
}
this._m1.delete(key);
this._m2.delete(value);
return true;
}
forEach(callbackfn: (value: V, key: K, map: BidirectionalMap<K, V>) => void, thisArg?: any): void {
this._m1.forEach((value, key) => {
callbackfn.call(thisArg, value, key, this);
});
}
keys(): IterableIterator<K> {
return this._m1.keys();
}
values(): IterableIterator<V> {
return this._m1.values();
}
}
+2 -1
View File
@@ -37,7 +37,8 @@ export {
debouncedObservable,
derivedObservableWithCache,
derivedObservableWithWritableCache,
keepAlive,
keepObserved,
recomputeInitiallyAndOnChange,
observableFromEvent,
observableFromPromise,
observableSignal,
+78 -21
View File
@@ -4,11 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import type { derived } from 'vs/base/common/observableInternal/derived';
import type { derivedOpts } from 'vs/base/common/observableInternal/derived';
import { getLogger } from 'vs/base/common/observableInternal/logging';
/**
* Represents an observable value.
*
* @template T The type of the value.
* @template TChange The type of delta information (usually `void` and only used in advanced scenarios).
*/
@@ -118,6 +119,10 @@ export interface IObserver {
}
export interface ISettable<T, TChange = void> {
/**
* Sets the value of the observable.
* Use a transaction to batch multiple changes (with a transaction, observers only react at the end of the transaction).
*/
set(value: T, transaction: ITransaction | undefined, change: TChange): void;
}
@@ -129,12 +134,12 @@ export interface ITransaction {
updateObserver(observer: IObserver, observable: IObservable<any, any>): void;
}
let _derived: typeof derived;
let _derived: typeof derivedOpts;
/**
* @internal
* This is to allow splitting files.
*/
export function _setDerived(derived: typeof _derived) {
export function _setDerivedOpts(derived: typeof _derived) {
_derived = derived;
}
@@ -162,21 +167,23 @@ export abstract class ConvenientObservable<T, TChange> implements IObservable<T,
/** @sealed */
public map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew> {
return _derived(
(reader) => fn(this.read(reader), reader),
() => {
const name = getFunctionName(fn);
if (name !== undefined) {
return name;
}
{
debugName: () => {
const name = getFunctionName(fn);
if (name !== undefined) {
return name;
}
// regexp to match `x => x.y` where x and y can be arbitrary identifiers (uses backref):
const regexp = /^\s*\(?\s*([a-zA-Z_$][a-zA-Z_$0-9]*)\s*\)?\s*=>\s*\1\.([a-zA-Z_$][a-zA-Z_$0-9]*)\s*$/;
const match = regexp.exec(fn.toString());
if (match) {
return `${this.debugName}.${match[2]}`;
}
return `${this.debugName} (mapped)`;
// regexp to match `x => x.y` where x and y can be arbitrary identifiers (uses backref):
const regexp = /^\s*\(?\s*([a-zA-Z_$][a-zA-Z_$0-9]*)\s*\)?\s*=>\s*\1\.([a-zA-Z_$][a-zA-Z_$0-9]*)\s*$/;
const match = regexp.exec(fn.toString());
if (match) {
return `${this.debugName}.${match[2]}`;
}
return `${this.debugName} (mapped)`;
},
},
(reader) => fn(this.read(reader), reader),
);
}
@@ -252,6 +259,38 @@ export class TransactionImpl implements ITransaction {
}
}
export type DebugNameFn = string | (() => string | undefined);
export function getDebugName(debugNameFn: DebugNameFn | undefined, fn: Function | undefined, owner: object | undefined, self: object): string | undefined {
let result: string | undefined;
if (debugNameFn !== undefined) {
if (typeof debugNameFn === 'function') {
result = debugNameFn();
if (result !== undefined) {
return result;
}
} else {
return debugNameFn;
}
}
if (fn !== undefined) {
result = getFunctionName(fn);
if (result !== undefined) {
return result;
}
}
if (owner !== undefined) {
for (const key in owner) {
if ((owner as any)[key] === self) {
return key;
}
}
}
return undefined;
}
export function getFunctionName(fn: Function): string | undefined {
const fnSrc = fn.toString();
// Pattern: /** @description ... */
@@ -268,8 +307,14 @@ export interface ISettableObservable<T, TChange = void> extends IObservable<T, T
* Creates an observable value.
* Observers get informed when the value changes.
*/
export function observableValue<T, TChange = void>(name: string, initialValue: T): ISettableObservable<T, TChange> {
return new ObservableValue(name, initialValue);
export function observableValue<T, TChange = void>(name: string, initialValue: T): ISettableObservable<T, TChange>;
export function observableValue<T, TChange = void>(owner: object, initialValue: T): ISettableObservable<T, TChange>;
export function observableValue<T, TChange = void>(nameOrOwner: string | object, initialValue: T): ISettableObservable<T, TChange> {
if (typeof nameOrOwner === 'string') {
return new ObservableValue(undefined, nameOrOwner, initialValue);
} else {
return new ObservableValue(nameOrOwner, undefined, initialValue);
}
}
export class ObservableValue<T, TChange = void>
@@ -278,7 +323,15 @@ export class ObservableValue<T, TChange = void>
{
protected _value: T;
constructor(public readonly debugName: string, initialValue: T) {
get debugName() {
return getDebugName(this._debugName, undefined, this._owner, this) ?? 'ObservableValue';
}
constructor(
private readonly _owner: object | undefined,
private readonly _debugName: string | undefined,
initialValue: T
) {
super();
this._value = initialValue;
}
@@ -320,8 +373,12 @@ export class ObservableValue<T, TChange = void>
}
}
export function disposableObservableValue<T extends IDisposable | undefined, TChange = void>(name: string, initialValue: T): ISettableObservable<T, TChange> & IDisposable {
return new DisposableObservableValue(name, initialValue);
export function disposableObservableValue<T extends IDisposable | undefined, TChange = void>(nameOrOwner: string | object, initialValue: T): ISettableObservable<T, TChange> & IDisposable {
if (typeof nameOrOwner === 'string') {
return new DisposableObservableValue(undefined, nameOrOwner, initialValue);
} else {
return new DisposableObservableValue(nameOrOwner, undefined, initialValue);
}
}
export class DisposableObservableValue<T extends IDisposable | undefined, TChange = void> extends ObservableValue<T, TChange> implements IDisposable {
@@ -5,39 +5,76 @@
import { BugIndicatingError } from 'vs/base/common/errors';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IReader, IObservable, BaseObservable, IObserver, _setDerived, IChangeContext, getFunctionName } from 'vs/base/common/observableInternal/base';
import { IReader, IObservable, BaseObservable, IObserver, _setDerivedOpts, IChangeContext, getFunctionName, DebugNameFn, getDebugName } from 'vs/base/common/observableInternal/base';
import { getLogger } from 'vs/base/common/observableInternal/logging';
export type EqualityComparer<T> = (a: T, b: T) => boolean;
const defaultEqualityComparer: EqualityComparer<any> = (a, b) => a === b;
export function derived<T>(computeFn: (reader: IReader) => T, debugName?: string | (() => string)): IObservable<T> {
return new Derived(debugName, computeFn, undefined, undefined, undefined, defaultEqualityComparer);
/**
* Creates an observable that is derived from other observables.
*/
export function derived<T>(computeFn: (reader: IReader) => T): IObservable<T>;
export function derived<T>(owner: object, computeFn: (reader: IReader) => T): IObservable<T>;
export function derived<T>(computeFnOrOwner: ((reader: IReader) => T) | object, computeFn?: ((reader: IReader) => T) | undefined): IObservable<T> {
if (computeFn !== undefined) {
return new Derived(computeFnOrOwner, undefined, computeFn, undefined, undefined, undefined, defaultEqualityComparer);
}
return new Derived(undefined, undefined, computeFnOrOwner as any, undefined, undefined, undefined, defaultEqualityComparer);
}
export function derivedOpts<T>(options: { debugName?: string | (() => string); equalityComparer?: EqualityComparer<T> }, computeFn: (reader: IReader) => T): IObservable<T> {
return new Derived(options.debugName, computeFn, undefined, undefined, undefined, options.equalityComparer ?? defaultEqualityComparer);
export function derivedOpts<T>(
options: {
owner?: object;
debugName?: string | (() => string);
equalityComparer?: EqualityComparer<T>;
},
computeFn: (reader: IReader) => T
): IObservable<T> {
return new Derived(options.owner, options.debugName, computeFn, undefined, undefined, undefined, options.equalityComparer ?? defaultEqualityComparer);
}
export function derivedHandleChanges<T, TChangeSummary>(
debugName: string | (() => string),
options: {
owner?: object;
debugName?: string | (() => string);
createEmptyChangeSummary: () => TChangeSummary;
handleChange: (context: IChangeContext, changeSummary: TChangeSummary) => boolean;
equalityComparer?: EqualityComparer<T>;
},
computeFn: (reader: IReader, changeSummary: TChangeSummary) => T): IObservable<T> {
return new Derived(debugName, computeFn, options.createEmptyChangeSummary, options.handleChange, undefined, defaultEqualityComparer);
computeFn: (reader: IReader, changeSummary: TChangeSummary) => T
): IObservable<T> {
return new Derived(options.owner, options.debugName, computeFn, options.createEmptyChangeSummary, options.handleChange, undefined, options.equalityComparer ?? defaultEqualityComparer);
}
export function derivedWithStore<T>(name: string, computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T> {
export function derivedWithStore<T>(computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
export function derivedWithStore<T>(owner: object, computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
export function derivedWithStore<T>(computeFnOrOwner: ((reader: IReader, store: DisposableStore) => T) | object, computeFnOrUndefined?: ((reader: IReader, store: DisposableStore) => T)): IObservable<T> {
let computeFn: (reader: IReader, store: DisposableStore) => T;
let owner: object | undefined;
if (computeFnOrUndefined === undefined) {
computeFn = computeFnOrOwner as any;
owner = undefined;
} else {
owner = computeFnOrOwner;
computeFn = computeFnOrUndefined as any;
}
const store = new DisposableStore();
return new Derived(name, r => {
store.clear();
return computeFn(r, store);
}, undefined, undefined, () => store.dispose(), defaultEqualityComparer);
return new Derived(
owner,
(() => getFunctionName(computeFn) ?? '(anonymous)'),
r => {
store.clear();
return computeFn(r, store);
}, undefined,
undefined,
() => store.dispose(),
defaultEqualityComparer
);
}
_setDerived(derived);
_setDerivedOpts(derived);
const enum DerivedState {
/** Initial state, no previous value, recomputation needed */
@@ -70,14 +107,12 @@ export class Derived<T, TChangeSummary = any> extends BaseObservable<T, void> im
private changeSummary: TChangeSummary | undefined = undefined;
public override get debugName(): string {
if (!this._debugName) {
return getFunctionName(this._computeFn) || '(anonymous)';
}
return typeof this._debugName === 'function' ? this._debugName() : this._debugName;
return getDebugName(this._debugName, this._computeFn, this._owner, this) ?? '(anonymous)';
}
constructor(
private readonly _debugName: string | (() => string) | undefined,
private readonly _owner: object | undefined,
private readonly _debugName: DebugNameFn | undefined,
public readonly _computeFn: (reader: IReader, changeSummary: TChangeSummary) => T,
private readonly createChangeSummary: (() => TChangeSummary) | undefined,
private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined,
+36 -25
View File
@@ -6,7 +6,7 @@
import { Event } from 'vs/base/common/event';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { autorun } from 'vs/base/common/observableInternal/autorun';
import { BaseObservable, ConvenientObservable, IObservable, IObserver, IReader, ITransaction, getFunctionName, observableValue, transaction } from 'vs/base/common/observableInternal/base';
import { BaseObservable, ConvenientObservable, IObservable, IObserver, IReader, ITransaction, getDebugName, getFunctionName, observableValue, transaction } from 'vs/base/common/observableInternal/base';
import { derived } from 'vs/base/common/observableInternal/derived';
import { getLogger } from 'vs/base/common/observableInternal/logging';
@@ -207,10 +207,14 @@ class FromEventObservableSignal extends BaseObservable<void> {
* Signals don't have a value - when they are triggered they indicate a change.
* However, signals can carry a delta that is passed to observers.
*/
export function observableSignal<TDelta = void>(
debugName: string
): IObservableSignal<TDelta> {
return new ObservableSignal<TDelta>(debugName);
export function observableSignal<TDelta = void>(debugName: string): IObservableSignal<TDelta>;
export function observableSignal<TDelta = void>(owner: object): IObservableSignal<TDelta>;
export function observableSignal<TDelta = void>(debugNameOrOwner: string | object): IObservableSignal<TDelta> {
if (typeof debugNameOrOwner === 'string') {
return new ObservableSignal<TDelta>(debugNameOrOwner);
} else {
return new ObservableSignal<TDelta>(undefined, debugNameOrOwner);
}
}
export interface IObservableSignal<TChange> extends IObservable<void, TChange> {
@@ -218,8 +222,13 @@ export interface IObservableSignal<TChange> extends IObservable<void, TChange> {
}
class ObservableSignal<TChange> extends BaseObservable<void, TChange> implements IObservableSignal<TChange> {
public get debugName() {
return getDebugName(this._debugName, undefined, this._owner, this) ?? 'Observable Signal';
}
constructor(
public readonly debugName: string
private readonly _debugName: string | undefined,
private readonly _owner?: object,
) {
super();
}
@@ -285,22 +294,24 @@ export function wasEventTriggeredRecently(event: Event<any>, timeoutMs: number,
return observable;
}
// TODO@hediet: Have `keepCacheAlive` and `recomputeOnChange` instead of forceRecompute
/**
* This ensures the observable is being observed.
* Observed observables (such as {@link derived}s) can maintain a cache, as they receive invalidation events.
* Unobserved observables are forced to recompute their value from scratch every time they are read.
*
* @param observable the observable to keep alive
* @param forceRecompute if true, the observable will be eagerly recomputed after it changed.
* Use this if recomputing the observables causes side-effects.
*/
export function keepAlive(observable: IObservable<any>, forceRecompute?: boolean): IDisposable {
const o = new KeepAliveObserver(forceRecompute ?? false);
* This makes sure the observable is being observed and keeps its cache alive.
*/
export function keepObserved<T>(observable: IObservable<T>): IDisposable {
const o = new KeepAliveObserver(false);
observable.addObserver(o);
if (forceRecompute) {
observable.reportChanges();
}
return toDisposable(() => {
observable.removeObserver(o);
});
}
/**
* This converts the given observable into an autorun.
*/
export function recomputeInitiallyAndOnChange<T>(observable: IObservable<T>): IDisposable {
const o = new KeepAliveObserver(true);
observable.addObserver(o);
observable.reportChanges();
return toDisposable(() => {
observable.removeObserver(o);
@@ -332,23 +343,23 @@ class KeepAliveObserver implements IObserver {
}
}
export function derivedObservableWithCache<T>(name: string, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> {
export function derivedObservableWithCache<T>(computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> {
let lastValue: T | undefined = undefined;
const observable = derived(reader => {
lastValue = computeFn(reader, lastValue);
return lastValue;
}, name);
});
return observable;
}
export function derivedObservableWithWritableCache<T>(name: string, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> & { clearCache(transaction: ITransaction): void } {
export function derivedObservableWithWritableCache<T>(owner: object, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> & { clearCache(transaction: ITransaction): void } {
let lastValue: T | undefined = undefined;
const counter = observableValue('derivedObservableWithWritableCache.counter', 0);
const observable = derived(reader => {
const observable = derived(owner, reader => {
counter.read(reader);
lastValue = computeFn(reader, lastValue);
return lastValue;
}, name);
});
return Object.assign(observable, {
clearCache: (transaction: ITransaction) => {
lastValue = undefined;
+11 -10
View File
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as arrays from 'vs/base/common/arrays';
import * as arraysFind from 'vs/base/common/arraysFind';
suite('Arrays', () => {
@@ -22,25 +23,25 @@ suite('Arrays', () => {
test('findFirst', () => {
const array = [1, 4, 5, 7, 55, 59, 60, 61, 64, 69];
let idx = arrays.findFirstInSorted(array, e => e >= 0);
let idx = arraysFind.findFirstIdxMonotonousOrArrLen(array, e => e >= 0);
assert.strictEqual(array[idx], 1);
idx = arrays.findFirstInSorted(array, e => e > 1);
idx = arraysFind.findFirstIdxMonotonousOrArrLen(array, e => e > 1);
assert.strictEqual(array[idx], 4);
idx = arrays.findFirstInSorted(array, e => e >= 8);
idx = arraysFind.findFirstIdxMonotonousOrArrLen(array, e => e >= 8);
assert.strictEqual(array[idx], 55);
idx = arrays.findFirstInSorted(array, e => e >= 61);
idx = arraysFind.findFirstIdxMonotonousOrArrLen(array, e => e >= 61);
assert.strictEqual(array[idx], 61);
idx = arrays.findFirstInSorted(array, e => e >= 69);
idx = arraysFind.findFirstIdxMonotonousOrArrLen(array, e => e >= 69);
assert.strictEqual(array[idx], 69);
idx = arrays.findFirstInSorted(array, e => e >= 70);
idx = arraysFind.findFirstIdxMonotonousOrArrLen(array, e => e >= 70);
assert.strictEqual(idx, array.length);
idx = arrays.findFirstInSorted([], e => e >= 0);
idx = arraysFind.findFirstIdxMonotonousOrArrLen([], e => e >= 0);
assert.strictEqual(array[idx], 1);
});
@@ -372,7 +373,7 @@ suite('Arrays', () => {
const array = [{ v: 3 }, { v: 5 }, { v: 2 }, { v: 2 }, { v: 2 }, { v: 5 }];
assert.strictEqual(
array.indexOf(arrays.findMaxBy(array, arrays.compareBy(v => v.v, arrays.numberComparator))!),
array.indexOf(arraysFind.findFirstMaxBy(array, arrays.compareBy(v => v.v, arrays.numberComparator))!),
1
);
});
@@ -381,7 +382,7 @@ suite('Arrays', () => {
const array = [{ v: 3 }, { v: 5 }, { v: 2 }, { v: 2 }, { v: 2 }, { v: 5 }];
assert.strictEqual(
array.indexOf(arrays.findLastMaxBy(array, arrays.compareBy(v => v.v, arrays.numberComparator))!),
array.indexOf(arraysFind.findLastMaxBy(array, arrays.compareBy(v => v.v, arrays.numberComparator))!),
5
);
});
@@ -390,7 +391,7 @@ suite('Arrays', () => {
const array = [{ v: 3 }, { v: 5 }, { v: 2 }, { v: 2 }, { v: 2 }, { v: 5 }];
assert.strictEqual(
array.indexOf(arrays.findMinBy(array, arrays.compareBy(v => v.v, arrays.numberComparator))!),
array.indexOf(arraysFind.findFirstMinBy(array, arrays.compareBy(v => v.v, arrays.numberComparator))!),
2
);
});
@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { MonotonousArray, findFirstMonotonous, findLastMonotonous } from 'vs/base/common/arraysFind';
suite('Arrays', () => {
test('findLastMonotonous', () => {
const array = [1, 4, 5, 7, 55, 59, 60, 61, 64, 69];
const result = findLastMonotonous(array, n => n <= 60);
assert.strictEqual(result, 60);
const result2 = findLastMonotonous(array, n => n <= 62);
assert.strictEqual(result2, 61);
const result3 = findLastMonotonous(array, n => n <= 1);
assert.strictEqual(result3, 1);
const result4 = findLastMonotonous(array, n => n <= 70);
assert.strictEqual(result4, 69);
const result5 = findLastMonotonous(array, n => n <= 0);
assert.strictEqual(result5, undefined);
});
test('findFirstMonotonous', () => {
const array = [1, 4, 5, 7, 55, 59, 60, 61, 64, 69];
const result = findFirstMonotonous(array, n => n >= 60);
assert.strictEqual(result, 60);
const result2 = findFirstMonotonous(array, n => n >= 62);
assert.strictEqual(result2, 64);
const result3 = findFirstMonotonous(array, n => n >= 1);
assert.strictEqual(result3, 1);
const result4 = findFirstMonotonous(array, n => n >= 70);
assert.strictEqual(result4, undefined);
const result5 = findFirstMonotonous(array, n => n >= 0);
assert.strictEqual(result5, 1);
});
test('MonotonousArray', () => {
const arr = new MonotonousArray([1, 4, 5, 7, 55, 59, 60, 61, 64, 69]);
assert.strictEqual(arr.findLastMonotonous(n => n <= 0), undefined);
assert.strictEqual(arr.findLastMonotonous(n => n <= 0), undefined);
assert.strictEqual(arr.findLastMonotonous(n => n <= 5), 5);
assert.strictEqual(arr.findLastMonotonous(n => n <= 6), 5);
assert.strictEqual(arr.findLastMonotonous(n => n <= 55), 55);
assert.strictEqual(arr.findLastMonotonous(n => n <= 60), 60);
assert.strictEqual(arr.findLastMonotonous(n => n <= 80), 69);
});
});
+78
View File
@@ -1507,4 +1507,82 @@ suite('Event utils', () => {
assert.deepStrictEqual(calls, [1]);
});
});
suite('chain2', () => {
let store: DisposableStore;
let em: Emitter<number>;
let calls: number[];
teardown(() => {
store.dispose();
});
ensureNoDisposablesAreLeakedInTestSuite();
setup(() => {
store = new DisposableStore();
em = new Emitter<number>();
store.add(em);
calls = [];
});
test('maps', () => {
const ev = Event.chain(em.event, $ => $.map(v => v * 2));
store.add(ev(v => calls.push(v)));
em.fire(1);
em.fire(2);
em.fire(3);
assert.deepStrictEqual(calls, [2, 4, 6]);
});
test('filters', () => {
const ev = Event.chain(em.event, $ => $.filter(v => v % 2 === 0));
store.add(ev(v => calls.push(v)));
em.fire(1);
em.fire(2);
em.fire(3);
em.fire(4);
assert.deepStrictEqual(calls, [2, 4]);
});
test('reduces', () => {
const ev = Event.chain(em.event, $ => $.reduce((acc, v) => acc + v, 0));
store.add(ev(v => calls.push(v)));
em.fire(1);
em.fire(2);
em.fire(3);
em.fire(4);
assert.deepStrictEqual(calls, [1, 3, 6, 10]);
});
test('latches', () => {
const ev = Event.chain(em.event, $ => $.latch());
store.add(ev(v => calls.push(v)));
em.fire(1);
em.fire(1);
em.fire(2);
em.fire(2);
em.fire(3);
em.fire(3);
em.fire(1);
assert.deepStrictEqual(calls, [1, 2, 3, 1]);
});
test('does everything', () => {
const ev = Event.chain(em.event, $ => $
.filter(v => v % 2 === 0)
.map(v => v * 2)
.reduce((acc, v) => acc + v, 0)
.latch()
);
store.add(ev(v => calls.push(v)));
em.fire(1);
em.fire(2);
em.fire(3);
em.fire(4);
em.fire(0);
assert.deepStrictEqual(calls, [4, 12]);
});
});
});
+87 -1
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { LinkedMap, LRUCache, ResourceMap, Touch } from 'vs/base/common/map';
import { BidirectionalMap, LinkedMap, LRUCache, ResourceMap, Touch } from 'vs/base/common/map';
import { extUriIgnorePathCase } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
@@ -485,3 +485,89 @@ suite('Map', () => {
});
});
suite('BidirectionalMap', () => {
test('should set and get values correctly', () => {
const map = new BidirectionalMap<string, number>();
map.set('one', 1);
map.set('two', 2);
map.set('three', 3);
assert.strictEqual(map.get('one'), 1);
assert.strictEqual(map.get('two'), 2);
assert.strictEqual(map.get('three'), 3);
});
test('should get keys by value correctly', () => {
const map = new BidirectionalMap<string, number>();
map.set('one', 1);
map.set('two', 2);
map.set('three', 3);
assert.strictEqual(map.getKey(1), 'one');
assert.strictEqual(map.getKey(2), 'two');
assert.strictEqual(map.getKey(3), 'three');
});
test('should delete values correctly', () => {
const map = new BidirectionalMap<string, number>();
map.set('one', 1);
map.set('two', 2);
map.set('three', 3);
assert.strictEqual(map.delete('one'), true);
assert.strictEqual(map.get('one'), undefined);
assert.strictEqual(map.getKey(1), undefined);
assert.strictEqual(map.delete('two'), true);
assert.strictEqual(map.get('two'), undefined);
assert.strictEqual(map.getKey(2), undefined);
assert.strictEqual(map.delete('three'), true);
assert.strictEqual(map.get('three'), undefined);
assert.strictEqual(map.getKey(3), undefined);
});
test('should handle non-existent keys correctly', () => {
const map = new BidirectionalMap<string, number>();
map.set('one', 1);
map.set('two', 2);
map.set('three', 3);
assert.strictEqual(map.get('four'), undefined);
assert.strictEqual(map.getKey(4), undefined);
assert.strictEqual(map.delete('four'), false);
});
test('should handle forEach correctly', () => {
const map = new BidirectionalMap<string, number>();
map.set('one', 1);
map.set('two', 2);
map.set('three', 3);
const keys: string[] = [];
const values: number[] = [];
map.forEach((value, key) => {
keys.push(key);
values.push(value);
});
assert.deepStrictEqual(keys, ['one', 'two', 'three']);
assert.deepStrictEqual(values, [1, 2, 3]);
});
test('should handle clear correctly', () => {
const map = new BidirectionalMap<string, number>();
map.set('one', 1);
map.set('two', 2);
map.set('three', 3);
map.clear();
assert.strictEqual(map.get('one'), undefined);
assert.strictEqual(map.get('two'), undefined);
assert.strictEqual(map.get('three'), undefined);
assert.strictEqual(map.getKey(1), undefined);
assert.strictEqual(map.getKey(2), undefined);
assert.strictEqual(map.getKey(3), undefined);
});
});
+2 -2
View File
@@ -5,7 +5,7 @@
import * as assert from 'assert';
import { Emitter, Event } from 'vs/base/common/event';
import { ISettableObservable, autorun, derived, ITransaction, observableFromEvent, observableValue, transaction, keepAlive } from 'vs/base/common/observable';
import { ISettableObservable, autorun, derived, ITransaction, observableFromEvent, observableValue, transaction, keepObserved } from 'vs/base/common/observable';
import { BaseObservable, IObservable, IObserver } from 'vs/base/common/observableInternal/base';
suite('observables', () => {
@@ -205,7 +205,7 @@ suite('observables', () => {
'value: 5',
]);
const disposable = keepAlive(computedSum); // Use keepAlive to keep the cache
const disposable = keepObserved(computedSum); // Use keepAlive to keep the cache
log.log(`value: ${computedSum.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute1: 1 % 3 = 1',
+17 -4
View File
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable, IDisposableTracker, setDisposableTracker } from 'vs/base/common/lifecycle';
import { DisposableStore, IDisposable, IDisposableTracker, setDisposableTracker } from 'vs/base/common/lifecycle';
import { join } from 'vs/base/common/path';
import { isWindows } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
@@ -111,7 +111,7 @@ export class DisposableTracker implements IDisposableTracker {
const firstLeaking = leaking.slice(0, count);
const remainingCount = leaking.length - count;
const separator = '--------------------\n\n';
const separator = '\n--------------------\n\n';
let s = firstLeaking.map(l => l.source).join(separator);
if (remainingCount > 0) {
s += `${separator}+ ${remainingCount} more`;
@@ -128,21 +128,34 @@ export class DisposableTracker implements IDisposableTracker {
*
* Use `markAsSingleton` if disposable singletons are created lazily that are allowed to outlive the test.
* Make sure that the singleton properly registers all child disposables so that they are excluded too.
*
* @returns A {@link DisposableStore} that can optionally be used to track disposables in the test.
* This will be automatically disposed on test teardown.
*/
export function ensureNoDisposablesAreLeakedInTestSuite() {
export function ensureNoDisposablesAreLeakedInTestSuite(): Pick<DisposableStore, 'add'> {
let tracker: DisposableTracker | undefined;
let store: DisposableStore;
setup(() => {
store = new DisposableStore();
tracker = new DisposableTracker();
setDisposableTracker(tracker);
});
teardown(function (this: import('mocha').Context) {
store.dispose();
setDisposableTracker(null);
if (this.currentTest?.state !== 'failed') {
tracker!.ensureNoLeakingDisposables();
}
});
// Wrap store as the suite function is called before it's initialized
const testContext = {
add<T extends IDisposable>(o: T): T {
return store.add(o);
}
};
return testContext;
}
export function throwIfDisposablesAreLeaked(body: () => void): void {
+1 -1
View File
@@ -8,7 +8,7 @@ import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { Promises } from 'vs/base/node/pfs';
import { SnapshotContext, assertSnapshot } from 'vs/base/test/common/snapshot';
import { URI } from 'vs/base/common/uri';
import path = require('path');
import * as path from 'path';
import { assertThrowsAsync } from 'vs/base/test/common/utils';
// tests for snapshot are in Node so that we can use native FS operations to
+11 -1
View File
@@ -774,7 +774,17 @@ export class CodeApplication extends Disposable {
if (secondSlash !== -1) {
const authority = uri.path.substring(1, secondSlash);
const path = uri.path.substring(secondSlash);
const remoteUri = URI.from({ scheme: Schemas.vscodeRemote, authority, path, query: uri.query, fragment: uri.fragment });
let query = uri.query;
const params = new URLSearchParams(uri.query);
if (params.get('windowId') === '_blank') {
// Make sure to unset any `windowId=_blank` here
// https://github.com/microsoft/vscode/issues/191902
params.delete('windowId');
query = params.toString();
}
const remoteUri = URI.from({ scheme: Schemas.vscodeRemote, authority, path, query, fragment: uri.fragment });
if (hasWorkspaceFileExtension(path)) {
return { workspaceUri: remoteUri };
+4 -4
View File
@@ -251,10 +251,10 @@ class CodeMain {
Promise.all<string | undefined>([
environmentMainService.extensionsPath,
environmentMainService.codeCachePath,
environmentMainService.logsHome.fsPath,
userDataProfilesMainService.defaultProfile.globalStorageHome.fsPath,
environmentMainService.workspaceStorageHome.fsPath,
environmentMainService.localHistoryHome.fsPath,
environmentMainService.logsHome.with({ scheme: Schemas.file }).fsPath,
userDataProfilesMainService.defaultProfile.globalStorageHome.with({ scheme: Schemas.file }).fsPath,
environmentMainService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath,
environmentMainService.localHistoryHome.with({ scheme: Schemas.file }).fsPath,
environmentMainService.backupHome
].map(path => path ? FSPromises.mkdir(path, { recursive: true }) : undefined)),
+1 -1
View File
@@ -121,7 +121,7 @@ class CliMain extends Disposable {
// Init folders
await Promise.all([
environmentService.appSettingsHome.fsPath,
environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath,
environmentService.extensionsPath
].map(path => path ? Promises.mkdir(path, { recursive: true }) : undefined));
@@ -6,7 +6,9 @@
import { RunOnceScheduler } from 'vs/base/common/async';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { basename, dirname, joinPath } from 'vs/base/common/resources';
import { Schemas } from 'vs/base/common/network';
import { join } from 'vs/base/common/path';
import { basename, dirname } from 'vs/base/common/resources';
import { Promises } from 'vs/base/node/pfs';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ILogService } from 'vs/platform/log/common/log';
@@ -30,9 +32,8 @@ export class LogsDataCleaner extends Disposable {
try {
const currentLog = basename(this.environmentService.logsHome);
const logsRoot = dirname(this.environmentService.logsHome);
const logFiles = await Promises.readdir(logsRoot.fsPath);
const logsRoot = dirname(this.environmentService.logsHome.with({ scheme: Schemas.file })).fsPath;
const logFiles = await Promises.readdir(logsRoot);
const allSessions = logFiles.filter(logFile => /^\d{8}T\d{6}$/.test(logFile));
const oldSessions = allSessions.sort().filter(session => session !== currentLog);
@@ -41,7 +42,7 @@ export class LogsDataCleaner extends Disposable {
if (sessionsToDelete.length > 0) {
this.logService.trace(`[logs cleanup]: Removing log folders '${sessionsToDelete.join(', ')}'`);
await Promise.all(sessionsToDelete.map(sessionToDelete => Promises.rm(joinPath(logsRoot, sessionToDelete).fsPath)));
await Promise.all(sessionsToDelete.map(sessionToDelete => Promises.rm(join(logsRoot, sessionToDelete))));
}
} catch (error) {
onUnexpectedError(error);
@@ -15,6 +15,7 @@ import { EXTENSION_DEVELOPMENT_EMPTY_WINDOW_WORKSPACE } from 'vs/platform/worksp
import { NON_EMPTY_WORKSPACE_ID_LENGTH } from 'vs/platform/workspaces/node/workspaces';
import { INativeHostService } from 'vs/platform/native/common/native';
import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService';
import { Schemas } from 'vs/base/common/network';
export class UnusedWorkspaceStorageDataCleaner extends Disposable {
@@ -36,11 +37,12 @@ export class UnusedWorkspaceStorageDataCleaner extends Disposable {
this.logService.trace('[storage cleanup]: Starting to clean up workspace storage folders for unused empty workspaces.');
try {
const workspaceStorageFolders = await Promises.readdir(this.environmentService.workspaceStorageHome.fsPath);
const workspaceStorageHome = this.environmentService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath;
const workspaceStorageFolders = await Promises.readdir(workspaceStorageHome);
const storageClient = new StorageClient(this.mainProcessService.getChannel('storage'));
await Promise.all(workspaceStorageFolders.map(async workspaceStorageFolder => {
const workspaceStoragePath = join(this.environmentService.workspaceStorageHome.fsPath, workspaceStorageFolder);
const workspaceStoragePath = join(workspaceStorageHome, workspaceStorageFolder);
if (workspaceStorageFolder.length === NON_EMPTY_WORKSPACE_ID_LENGTH) {
return; // keep workspace storage for folders/workspaces that can be accessed still
@@ -26,7 +26,8 @@ import { IEditorWorkerHost } from 'vs/editor/common/services/editorWorkerHost';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { IChange } from 'vs/editor/common/diff/legacyLinesDiffComputer';
import { IDocumentDiff, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { ILinesDiffComputerOptions, LineRangeMapping, MovedText, RangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { ILinesDiffComputerOptions, MovedText } from 'vs/editor/common/diff/linesDiffComputer';
import { DetailedLineRangeMapping, RangeMapping, LineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { LineRange } from 'vs/editor/common/core/lineRange';
/**
@@ -107,15 +108,15 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ
quitEarly: result.quitEarly,
changes: toLineRangeMappings(result.changes),
moves: result.moves.map(m => new MovedText(
new SimpleLineRangeMapping(new LineRange(m[0], m[1]), new LineRange(m[2], m[3])),
new LineRangeMapping(new LineRange(m[0], m[1]), new LineRange(m[2], m[3])),
toLineRangeMappings(m[4])
))
};
return diff;
function toLineRangeMappings(changes: readonly ILineChange[]): readonly LineRangeMapping[] {
function toLineRangeMappings(changes: readonly ILineChange[]): readonly DetailedLineRangeMapping[] {
return changes.map(
(c) => new LineRangeMapping(
(c) => new DetailedLineRangeMapping(
new LineRange(c[0], c[1]),
new LineRange(c[2], c[3]),
c[4]?.map(
@@ -4,9 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { MinimapCharRenderer } from 'vs/editor/browser/viewParts/minimap/minimapCharRenderer';
import { allCharCodes } from 'vs/editor/browser/viewParts/minimap/minimapCharSheet';
import { allCharCodes, Constants } from 'vs/editor/browser/viewParts/minimap/minimapCharSheet';
import { prebakedMiniMaps } from 'vs/editor/browser/viewParts/minimap/minimapPreBaked';
import { Constants } from './minimapCharSheet';
import { toUint8 } from 'vs/base/common/uint';
/**
@@ -17,6 +17,7 @@ import { EditorTheme } from 'vs/editor/common/editorTheme';
import * as viewEvents from 'vs/editor/common/viewEvents';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { OverviewRulerDecorationsGroup } from 'vs/editor/common/viewModel';
import { equals } from 'vs/base/common/arrays';
class Settings {
@@ -212,13 +213,24 @@ const enum OverviewRulerLane {
Full = 7
}
const enum ShouldRenderValue {
NotNeeded = 0,
Maybe = 1,
Needed = 2
}
export class DecorationsOverviewRuler extends ViewPart {
private _actualShouldRender: ShouldRenderValue = ShouldRenderValue.NotNeeded;
private readonly _tokensColorTrackerListener: IDisposable;
private readonly _domNode: FastDomNode<HTMLCanvasElement>;
private _settings!: Settings;
private _cursorPositions: Position[];
private _renderedDecorations: OverviewRulerDecorationsGroup[] = [];
private _renderedCursorPositions: Position[] = [];
constructor(context: ViewContext) {
super(context);
@@ -270,8 +282,18 @@ export class DecorationsOverviewRuler extends ViewPart {
// ---- begin view event handlers
private _markRenderingIsNeeded(): true {
this._actualShouldRender = ShouldRenderValue.Needed;
return true;
}
private _markRenderingIsMaybeNeeded(): true {
this._actualShouldRender = ShouldRenderValue.Maybe;
return true;
}
public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
return this._updateSettings(false);
return this._updateSettings(false) ? this._markRenderingIsNeeded() : false;
}
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
this._cursorPositions = [];
@@ -279,25 +301,25 @@ export class DecorationsOverviewRuler extends ViewPart {
this._cursorPositions[i] = e.selections[i].getPosition();
}
this._cursorPositions.sort(Position.compare);
return true;
return this._markRenderingIsMaybeNeeded();
}
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
if (e.affectsOverviewRuler) {
return true;
return this._markRenderingIsMaybeNeeded();
}
return false;
}
public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
return this._markRenderingIsNeeded();
}
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollHeightChanged;
return e.scrollHeightChanged ? this._markRenderingIsNeeded() : false;
}
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
return this._markRenderingIsNeeded();
}
public override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
return this._updateSettings(false);
return this._updateSettings(false) ? this._markRenderingIsNeeded() : false;
}
// ---- end view event handlers
@@ -312,6 +334,7 @@ export class DecorationsOverviewRuler extends ViewPart {
public render(editorCtx: RestrictedRenderingContext): void {
this._render();
this._actualShouldRender = ShouldRenderValue.NotNeeded;
}
private _render(): void {
@@ -322,6 +345,23 @@ export class DecorationsOverviewRuler extends ViewPart {
this._domNode.setDisplay('none');
return;
}
const decorations = this._context.viewModel.getAllOverviewRulerDecorations(this._context.theme);
decorations.sort(OverviewRulerDecorationsGroup.cmp);
if (this._actualShouldRender === ShouldRenderValue.Maybe && !OverviewRulerDecorationsGroup.equalsArr(this._renderedDecorations, decorations)) {
this._actualShouldRender = ShouldRenderValue.Needed;
}
if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.lineNumber === b.lineNumber)) {
this._actualShouldRender = ShouldRenderValue.Needed;
}
if (this._actualShouldRender === ShouldRenderValue.Maybe) {
// both decorations and cursor positions are unchanged, nothing to do
return;
}
this._renderedDecorations = decorations;
this._renderedCursorPositions = this._cursorPositions;
this._domNode.setDisplay('block');
const canvasWidth = this._settings.canvasWidth;
const canvasHeight = this._settings.canvasHeight;
@@ -329,7 +369,6 @@ export class DecorationsOverviewRuler extends ViewPart {
const viewLayout = this._context.viewLayout;
const outerHeight = this._context.viewLayout.getScrollHeight();
const heightRatio = canvasHeight / outerHeight;
const decorations = this._context.viewModel.getAllOverviewRulerDecorations(this._context.theme);
const minDecorationHeight = (Constants.MIN_DECORATION_HEIGHT * this._settings.pixelRatio) | 0;
const halfMinDecorationHeight = (minDecorationHeight / 2) | 0;
@@ -355,7 +394,7 @@ export class DecorationsOverviewRuler extends ViewPart {
const x = this._settings.x;
const w = this._settings.w;
decorations.sort(OverviewRulerDecorationsGroup.cmp);
for (const decorationGroup of decorations) {
const color = decorationGroup.color;
File diff suppressed because it is too large Load Diff
@@ -4,24 +4,25 @@
*--------------------------------------------------------------------------------------------*/
import { addDisposableListener, addStandardDisposableListener, reset } from 'vs/base/browser/dom';
import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { Action } from 'vs/base/common/actions';
import { forEachAdjacent, groupAdjacentBy } from 'vs/base/common/arrays';
import { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, ITransaction, autorun, autorunWithStore, derived, derivedWithStore, keepAlive, observableValue, subtransaction, transaction } from 'vs/base/common/observable';
import { IObservable, ITransaction, autorun, autorunWithStore, derived, derivedWithStore, recomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from 'vs/base/common/observable';
import { ThemeIcon } from 'vs/base/common/themables';
import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo';
import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors';
import { applyStyle } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { DiffReview } from 'vs/editor/browser/widget/diffReview';
import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { LineRangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { DetailedLineRangeMapping, LineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { ILanguageIdCodec } from 'vs/editor/common/languages';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model';
@@ -38,6 +39,8 @@ const accessibleDiffViewerRemoveIcon = registerIcon('diff-review-remove', Codico
const accessibleDiffViewerCloseIcon = registerIcon('diff-review-close', Codicon.close, localize('accessibleDiffViewerCloseIcon', 'Icon for \'Close\' in accessible diff viewer.'));
export class AccessibleDiffViewer extends Disposable {
public static _ttPolicy = createTrustedTypesPolicy('diffReview', { createHTML: value => value });
constructor(
private readonly _parentNode: HTMLElement,
private readonly _visible: IObservable<boolean>,
@@ -45,15 +48,15 @@ export class AccessibleDiffViewer extends Disposable {
private readonly _canClose: IObservable<boolean>,
private readonly _width: IObservable<number>,
private readonly _height: IObservable<number>,
private readonly _diffs: IObservable<LineRangeMapping[] | undefined>,
private readonly _diffs: IObservable<DetailedLineRangeMapping[] | undefined>,
private readonly _editors: DiffEditorEditors,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();
this._register(keepAlive(this.model, true));
this._register(recomputeInitiallyAndOnChange(this.model));
}
private readonly model = derivedWithStore('model', (reader, store) => {
private readonly model = derivedWithStore(this, (reader, store) => {
const visible = this._visible.read(reader);
this._parentNode.style.visibility = visible ? 'visible' : 'hidden';
if (!visible) {
@@ -92,9 +95,9 @@ export class AccessibleDiffViewer extends Disposable {
}
class ViewModel extends Disposable {
private readonly _groups = observableValue<ViewElementGroup[]>('groups', []);
private readonly _currentGroupIdx = observableValue('currentGroupIdx', 0);
private readonly _currentElementIdx = observableValue('currentElementIdx', 0);
private readonly _groups = observableValue<ViewElementGroup[]>(this, []);
private readonly _currentGroupIdx = observableValue(this, 0);
private readonly _currentElementIdx = observableValue(this, 0);
public readonly groups: IObservable<ViewElementGroup[]> = this._groups;
public readonly currentGroup: IObservable<ViewElementGroup | undefined>
@@ -105,7 +108,7 @@ class ViewModel extends Disposable {
= this._currentElementIdx.map((idx, r) => this.currentGroup.read(r)?.lines[idx]);
constructor(
private readonly _diffs: IObservable<LineRangeMapping[] | undefined>,
private readonly _diffs: IObservable<DetailedLineRangeMapping[] | undefined>,
private readonly _editors: DiffEditorEditors,
private readonly _setVisible: (visible: boolean, tx: ITransaction | undefined) => void,
public readonly canClose: IObservable<boolean>,
@@ -154,7 +157,7 @@ class ViewModel extends Disposable {
// This ensures editor commands (like revert/stage) work
const currentViewItem = this.currentElement.read(reader);
if (currentViewItem && currentViewItem.type !== LineType.Header) {
const lineNumber = currentViewItem.modifiedLineNumber ?? currentViewItem.diff.modifiedRange.startLineNumber;
const lineNumber = currentViewItem.modifiedLineNumber ?? currentViewItem.diff.modified.startLineNumber;
this._editors.modified.setSelection(Range.fromPositions(new Position(lineNumber, 1)));
}
}));
@@ -221,44 +224,44 @@ class ViewModel extends Disposable {
const viewElementGroupLineMargin = 3;
function computeViewElementGroups(diffs: LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): ViewElementGroup[] {
function computeViewElementGroups(diffs: DetailedLineRangeMapping[], originalLineCount: number, modifiedLineCount: number): ViewElementGroup[] {
const result: ViewElementGroup[] = [];
for (const g of group(diffs, (a, b) => (b.modifiedRange.startLineNumber - a.modifiedRange.endLineNumberExclusive < 2 * viewElementGroupLineMargin))) {
for (const g of groupAdjacentBy(diffs, (a, b) => (b.modified.startLineNumber - a.modified.endLineNumberExclusive < 2 * viewElementGroupLineMargin))) {
const viewElements: ViewElement[] = [];
viewElements.push(new HeaderViewElement());
const origFullRange = new LineRange(
Math.max(1, g[0].originalRange.startLineNumber - viewElementGroupLineMargin),
Math.min(g[g.length - 1].originalRange.endLineNumberExclusive + viewElementGroupLineMargin, originalLineCount + 1)
Math.max(1, g[0].original.startLineNumber - viewElementGroupLineMargin),
Math.min(g[g.length - 1].original.endLineNumberExclusive + viewElementGroupLineMargin, originalLineCount + 1)
);
const modifiedFullRange = new LineRange(
Math.max(1, g[0].modifiedRange.startLineNumber - viewElementGroupLineMargin),
Math.min(g[g.length - 1].modifiedRange.endLineNumberExclusive + viewElementGroupLineMargin, modifiedLineCount + 1)
Math.max(1, g[0].modified.startLineNumber - viewElementGroupLineMargin),
Math.min(g[g.length - 1].modified.endLineNumberExclusive + viewElementGroupLineMargin, modifiedLineCount + 1)
);
forEachAdjacentItems(g, (a, b) => {
const origRange = new LineRange(a ? a.originalRange.endLineNumberExclusive : origFullRange.startLineNumber, b ? b.originalRange.startLineNumber : origFullRange.endLineNumberExclusive);
const modifiedRange = new LineRange(a ? a.modifiedRange.endLineNumberExclusive : modifiedFullRange.startLineNumber, b ? b.modifiedRange.startLineNumber : modifiedFullRange.endLineNumberExclusive);
forEachAdjacent(g, (a, b) => {
const origRange = new LineRange(a ? a.original.endLineNumberExclusive : origFullRange.startLineNumber, b ? b.original.startLineNumber : origFullRange.endLineNumberExclusive);
const modifiedRange = new LineRange(a ? a.modified.endLineNumberExclusive : modifiedFullRange.startLineNumber, b ? b.modified.startLineNumber : modifiedFullRange.endLineNumberExclusive);
origRange.forEach(origLineNumber => {
viewElements.push(new UnchangedLineViewElement(origLineNumber, modifiedRange.startLineNumber + (origLineNumber - origRange.startLineNumber)));
});
if (b) {
b.originalRange.forEach(origLineNumber => {
b.original.forEach(origLineNumber => {
viewElements.push(new DeletedLineViewElement(b, origLineNumber));
});
b.modifiedRange.forEach(modifiedLineNumber => {
b.modified.forEach(modifiedLineNumber => {
viewElements.push(new AddedLineViewElement(b, modifiedLineNumber));
});
}
});
const modifiedRange = g[0].modifiedRange.join(g[g.length - 1].modifiedRange);
const originalRange = g[0].originalRange.join(g[g.length - 1].originalRange);
const modifiedRange = g[0].modified.join(g[g.length - 1].modified);
const originalRange = g[0].original.join(g[g.length - 1].original);
result.push(new ViewElementGroup(new SimpleLineRangeMapping(modifiedRange, originalRange), viewElements));
result.push(new ViewElementGroup(new LineRangeMapping(modifiedRange, originalRange), viewElements));
}
return result;
}
@@ -272,7 +275,7 @@ enum LineType {
class ViewElementGroup {
constructor(
public readonly range: SimpleLineRangeMapping,
public readonly range: LineRangeMapping,
public readonly lines: readonly ViewElement[],
) { }
}
@@ -289,7 +292,7 @@ class DeletedLineViewElement {
public readonly modifiedLineNumber = undefined;
constructor(
public readonly diff: LineRangeMapping,
public readonly diff: DetailedLineRangeMapping,
public readonly originalLineNumber: number,
) {
}
@@ -301,7 +304,7 @@ class AddedLineViewElement {
public readonly originalLineNumber = undefined;
constructor(
public readonly diff: LineRangeMapping,
public readonly diff: DetailedLineRangeMapping,
public readonly modifiedLineNumber: number,
) {
}
@@ -589,15 +592,15 @@ class View extends Disposable {
let lineContent: string;
if (item.modifiedLineNumber !== undefined) {
let html: string | TrustedHTML = this._getLineHtml(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, item.modifiedLineNumber, this._languageService.languageIdCodec);
if (DiffReview._ttPolicy) {
html = DiffReview._ttPolicy.createHTML(html as string);
if (AccessibleDiffViewer._ttPolicy) {
html = AccessibleDiffViewer._ttPolicy.createHTML(html as string);
}
cell.insertAdjacentHTML('beforeend', html as string);
lineContent = modifiedModel.getLineContent(item.modifiedLineNumber);
} else {
let html: string | TrustedHTML = this._getLineHtml(originalModel, originalOptions, originalModelOpts.tabSize, item.originalLineNumber, this._languageService.languageIdCodec);
if (DiffReview._ttPolicy) {
html = DiffReview._ttPolicy.createHTML(html as string);
if (AccessibleDiffViewer._ttPolicy) {
html = AccessibleDiffViewer._ttPolicy.createHTML(html as string);
}
cell.insertAdjacentHTML('beforeend', html as string);
lineContent = originalModel.getLineContent(item.originalLineNumber);
@@ -659,31 +662,3 @@ class View extends Disposable {
return r.html;
}
}
function forEachAdjacentItems<T>(items: T[], callback: (item1: T | undefined, item2: T | undefined) => void) {
let last: T | undefined;
for (const item of items) {
callback(last, item);
last = item;
}
callback(last, undefined);
}
function* group<T>(items: Iterable<T>, shouldBeGrouped: (item1: T, item2: T) => boolean): Iterable<T[]> {
let currentGroup: T[] | undefined;
let last: T | undefined;
for (const item of items) {
if (last !== undefined && shouldBeGrouped(last, item)) {
currentGroup!.push(item);
} else {
if (currentGroup) {
yield currentGroup;
}
currentGroup = [item];
}
last = item;
}
if (currentGroup) {
yield currentGroup;
}
}
@@ -27,8 +27,7 @@ export class DiffEditorDecorations extends Disposable {
this._register(applyObservableDecorations(this._editors.modified, this._decorations.map(d => d?.modifiedDecorations || [])));
}
private readonly _decorations = derived((reader) => {
/** @description _decorations */
private readonly _decorations = derived(this, (reader) => {
const diff = this._diffModel.read(reader)?.diff.read(reader);
if (!diff) {
return null;
@@ -42,45 +41,45 @@ export class DiffEditorDecorations extends Disposable {
const modifiedDecorations: IModelDeltaDecoration[] = [];
if (!movedTextToCompare) {
for (const m of diff.mappings) {
if (!m.lineRangeMapping.originalRange.isEmpty) {
originalDecorations.push({ range: m.lineRangeMapping.originalRange.toInclusiveRange()!, options: renderIndicators ? diffLineDeleteDecorationBackgroundWithIndicator : diffLineDeleteDecorationBackground });
if (!m.lineRangeMapping.original.isEmpty) {
originalDecorations.push({ range: m.lineRangeMapping.original.toInclusiveRange()!, options: renderIndicators ? diffLineDeleteDecorationBackgroundWithIndicator : diffLineDeleteDecorationBackground });
}
if (!m.lineRangeMapping.modifiedRange.isEmpty) {
modifiedDecorations.push({ range: m.lineRangeMapping.modifiedRange.toInclusiveRange()!, options: renderIndicators ? diffLineAddDecorationBackgroundWithIndicator : diffLineAddDecorationBackground });
if (!m.lineRangeMapping.modified.isEmpty) {
modifiedDecorations.push({ range: m.lineRangeMapping.modified.toInclusiveRange()!, options: renderIndicators ? diffLineAddDecorationBackgroundWithIndicator : diffLineAddDecorationBackground });
}
if (m.lineRangeMapping.modifiedRange.isEmpty || m.lineRangeMapping.originalRange.isEmpty) {
if (!m.lineRangeMapping.originalRange.isEmpty) {
originalDecorations.push({ range: m.lineRangeMapping.originalRange.toInclusiveRange()!, options: diffWholeLineDeleteDecoration });
if (m.lineRangeMapping.modified.isEmpty || m.lineRangeMapping.original.isEmpty) {
if (!m.lineRangeMapping.original.isEmpty) {
originalDecorations.push({ range: m.lineRangeMapping.original.toInclusiveRange()!, options: diffWholeLineDeleteDecoration });
}
if (!m.lineRangeMapping.modifiedRange.isEmpty) {
modifiedDecorations.push({ range: m.lineRangeMapping.modifiedRange.toInclusiveRange()!, options: diffWholeLineAddDecoration });
if (!m.lineRangeMapping.modified.isEmpty) {
modifiedDecorations.push({ range: m.lineRangeMapping.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration });
}
} else {
for (const i of m.lineRangeMapping.innerChanges || []) {
// Don't show empty markers outside the line range
if (m.lineRangeMapping.originalRange.contains(i.originalRange.startLineNumber)) {
if (m.lineRangeMapping.original.contains(i.originalRange.startLineNumber)) {
originalDecorations.push({ range: i.originalRange, options: (i.originalRange.isEmpty() && showEmptyDecorations) ? diffDeleteDecorationEmpty : diffDeleteDecoration });
}
if (m.lineRangeMapping.modifiedRange.contains(i.modifiedRange.startLineNumber)) {
if (m.lineRangeMapping.modified.contains(i.modifiedRange.startLineNumber)) {
modifiedDecorations.push({ range: i.modifiedRange, options: (i.modifiedRange.isEmpty() && showEmptyDecorations) ? diffAddDecorationEmpty : diffAddDecoration });
}
}
}
if (!m.lineRangeMapping.modifiedRange.isEmpty && this._options.shouldRenderRevertArrows.read(reader) && !movedTextToCompare) {
modifiedDecorations.push({ range: Range.fromPositions(new Position(m.lineRangeMapping.modifiedRange.startLineNumber, 1)), options: arrowRevertChange });
if (!m.lineRangeMapping.modified.isEmpty && this._options.shouldRenderRevertArrows.read(reader) && !movedTextToCompare) {
modifiedDecorations.push({ range: Range.fromPositions(new Position(m.lineRangeMapping.modified.startLineNumber, 1)), options: arrowRevertChange });
}
}
}
if (movedTextToCompare) {
for (const m of movedTextToCompare.changes) {
const fullRangeOriginal = m.originalRange.toInclusiveRange();
const fullRangeOriginal = m.original.toInclusiveRange();
if (fullRangeOriginal) {
originalDecorations.push({ range: fullRangeOriginal, options: renderIndicators ? diffLineDeleteDecorationBackgroundWithIndicator : diffLineDeleteDecorationBackground });
}
const fullRangeModified = m.modifiedRange.toInclusiveRange();
const fullRangeModified = m.modified.toInclusiveRange();
if (fullRangeModified) {
modifiedDecorations.push({ range: fullRangeModified, options: renderIndicators ? diffLineAddDecorationBackgroundWithIndicator : diffLineAddDecorationBackground });
}
@@ -4,11 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IReader, autorunHandleChanges } from 'vs/base/common/observable';
import { IObservable, IReader, autorunHandleChanges, observableFromEvent } from 'vs/base/common/observable';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
import { IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditorWidget';
import { OverviewRulerPart } from 'vs/editor/browser/widget/diffEditorWidget2/overviewRulerPart';
import { EditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IContentSizeChangedEvent } from 'vs/editor/common/editorCommon';
@@ -16,6 +15,8 @@ import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { DiffEditorOptions } from './diffEditorOptions';
import { ITextModel } from 'vs/editor/common/model';
import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2';
export class DiffEditorEditors extends Disposable {
public readonly modified: CodeEditorWidget;
@@ -24,6 +25,8 @@ export class DiffEditorEditors extends Disposable {
private readonly _onDidContentSizeChange = this._register(new Emitter<IContentSizeChangedEvent>());
public get onDidContentSizeChange() { return this._onDidContentSizeChange.event; }
public readonly modifiedModel: IObservable<ITextModel | null>;
constructor(
private readonly originalEditorElement: HTMLElement,
private readonly modifiedEditorElement: HTMLElement,
@@ -38,6 +41,8 @@ export class DiffEditorEditors extends Disposable {
this.original = this._register(this._createLeftHandSideEditor(_options.editorOptions.get(), codeEditorWidgetOptions.originalEditor || {}));
this.modified = this._register(this._createRightHandSideEditor(_options.editorOptions.get(), codeEditorWidgetOptions.modifiedEditor || {}));
this.modifiedModel = observableFromEvent(this.modified.onDidChangeModel, () => this.modified.getModel());
this._register(autorunHandleChanges({
createEmptyChangeSummary: () => ({} as IDiffEditorConstructionOptions),
handleChange: (ctx, changeSummary) => {
@@ -17,44 +17,43 @@ export class DiffEditorOptions {
constructor(options: Readonly<IDiffEditorConstructionOptions>, private readonly diffEditorWidth: IObservable<number>) {
const optionsCopy = { ...options, ...validateDiffEditorOptions(options, diffEditorDefaultOptions) };
this._options = observableValue('options', optionsCopy);
this._options = observableValue(this, optionsCopy);
}
public readonly couldShowInlineViewBecauseOfSize = derived(reader => /** @description couldShowInlineViewBecauseOfSize */ this._options.read(reader).renderSideBySide && this.diffEditorWidth.read(reader) <= this._options.read(reader).renderSideBySideInlineBreakpoint
public readonly couldShowInlineViewBecauseOfSize = derived(this, reader => this._options.read(reader).renderSideBySide && this.diffEditorWidth.read(reader) <= this._options.read(reader).renderSideBySideInlineBreakpoint
);
public readonly renderOverviewRuler = derived(reader => /** @description renderOverviewRuler */ this._options.read(reader).renderOverviewRuler);
public readonly renderSideBySide = derived(reader => /** @description renderSideBySide */ this._options.read(reader).renderSideBySide
public readonly renderOverviewRuler = derived(this, reader => this._options.read(reader).renderOverviewRuler);
public readonly renderSideBySide = derived(this, reader => this._options.read(reader).renderSideBySide
&& !(this._options.read(reader).useInlineViewWhenSpaceIsLimited && this.couldShowInlineViewBecauseOfSize.read(reader))
);
public readonly readOnly = derived(reader => /** @description readOnly */ this._options.read(reader).readOnly);
public readonly readOnly = derived(this, reader => this._options.read(reader).readOnly);
public readonly shouldRenderRevertArrows = derived(reader => {
/** @description shouldRenderRevertArrows */
public readonly shouldRenderRevertArrows = derived(this, reader => {
if (!this._options.read(reader).renderMarginRevertIcon) { return false; }
if (!this.renderSideBySide.read(reader)) { return false; }
if (this.readOnly.read(reader)) { return false; }
return true;
});
public readonly renderIndicators = derived(reader => /** @description renderIndicators */ this._options.read(reader).renderIndicators);
public readonly enableSplitViewResizing = derived(reader => /** @description enableSplitViewResizing */ this._options.read(reader).enableSplitViewResizing);
public readonly splitViewDefaultRatio = derived(reader => /** @description splitViewDefaultRatio */ this._options.read(reader).splitViewDefaultRatio);
public readonly ignoreTrimWhitespace = derived(reader => /** @description ignoreTrimWhitespace */ this._options.read(reader).ignoreTrimWhitespace);
public readonly maxComputationTimeMs = derived(reader => /** @description maxComputationTime */ this._options.read(reader).maxComputationTime);
public readonly showMoves = derived(reader => /** @description showMoves */ this._options.read(reader).experimental.showMoves! && this.renderSideBySide.read(reader));
public readonly isInEmbeddedEditor = derived(reader => /** @description isInEmbeddedEditor */ this._options.read(reader).isInEmbeddedEditor);
public readonly diffWordWrap = derived(reader => /** @description diffWordWrap */ this._options.read(reader).diffWordWrap);
public readonly originalEditable = derived(reader => /** @description originalEditable */ this._options.read(reader).originalEditable);
public readonly diffCodeLens = derived(reader => /** @description diffCodeLens */ this._options.read(reader).diffCodeLens);
public readonly accessibilityVerbose = derived(reader => /** @description accessibilityVerbose */ this._options.read(reader).accessibilityVerbose);
public readonly diffAlgorithm = derived(reader => /** @description diffAlgorithm */ this._options.read(reader).diffAlgorithm);
public readonly showEmptyDecorations = derived(reader => /** @description showEmptyDecorations */ this._options.read(reader).experimental.showEmptyDecorations!);
public readonly onlyShowAccessibleDiffViewer = derived(reader => /** @description onlyShowAccessibleDiffViewer */ this._options.read(reader).onlyShowAccessibleDiffViewer);
public readonly renderIndicators = derived(this, reader => this._options.read(reader).renderIndicators);
public readonly enableSplitViewResizing = derived(this, reader => this._options.read(reader).enableSplitViewResizing);
public readonly splitViewDefaultRatio = derived(this, reader => this._options.read(reader).splitViewDefaultRatio);
public readonly ignoreTrimWhitespace = derived(this, reader => this._options.read(reader).ignoreTrimWhitespace);
public readonly maxComputationTimeMs = derived(this, reader => this._options.read(reader).maxComputationTime);
public readonly showMoves = derived(this, reader => this._options.read(reader).experimental.showMoves! && this.renderSideBySide.read(reader));
public readonly isInEmbeddedEditor = derived(this, reader => this._options.read(reader).isInEmbeddedEditor);
public readonly diffWordWrap = derived(this, reader => this._options.read(reader).diffWordWrap);
public readonly originalEditable = derived(this, reader => this._options.read(reader).originalEditable);
public readonly diffCodeLens = derived(this, reader => this._options.read(reader).diffCodeLens);
public readonly accessibilityVerbose = derived(this, reader => this._options.read(reader).accessibilityVerbose);
public readonly diffAlgorithm = derived(this, reader => this._options.read(reader).diffAlgorithm);
public readonly showEmptyDecorations = derived(this, reader => this._options.read(reader).experimental.showEmptyDecorations!);
public readonly onlyShowAccessibleDiffViewer = derived(this, reader => this._options.read(reader).onlyShowAccessibleDiffViewer);
public readonly hideUnchangedRegions = derived(reader => /** @description hideUnchangedRegions */ this._options.read(reader).hideUnchangedRegions.enabled!);
public readonly hideUnchangedRegionsRevealLineCount = derived(reader => /** @description hideUnchangedRegions */ this._options.read(reader).hideUnchangedRegions.revealLineCount!);
public readonly hideUnchangedRegionsContextLineCount = derived(reader => /** @description hideUnchangedRegions */ this._options.read(reader).hideUnchangedRegions.contextLineCount!);
public readonly hideUnchangedRegionsminimumLineCount = derived(reader => /** @description hideUnchangedRegions */ this._options.read(reader).hideUnchangedRegions.minimumLineCount!);
public readonly hideUnchangedRegions = derived(this, reader => this._options.read(reader).hideUnchangedRegions.enabled!);
public readonly hideUnchangedRegionsRevealLineCount = derived(this, reader => this._options.read(reader).hideUnchangedRegions.revealLineCount!);
public readonly hideUnchangedRegionsContextLineCount = derived(this, reader => this._options.read(reader).hideUnchangedRegions.contextLineCount!);
public readonly hideUnchangedRegionsMinimumLineCount = derived(this, reader => this._options.read(reader).hideUnchangedRegions.minimumLineCount!);
public updateOptions(changedOptions: IDiffEditorOptions): void {
const newDiffEditorOptions = validateDiffEditorOptions(changedOptions, this._options.get());
@@ -9,10 +9,9 @@ import { IObservable, IReader, autorun, derived, observableValue } from 'vs/base
import { DiffEditorOptions } from './diffEditorOptions';
export class DiffEditorSash extends Disposable {
private readonly _sashRatio = observableValue<number | undefined>('sashRatio', undefined);
private readonly _sashRatio = observableValue<number | undefined>(this, undefined);
public readonly sashLeft = derived(reader => {
/** @description sashLeft */
public readonly sashLeft = derived(this, reader => {
const ratio = this._sashRatio.read(reader) ?? this._options.splitViewDefaultRatio.read(reader);
return this._computeSashLeft(ratio, reader);
});
@@ -7,11 +7,14 @@ import { RunOnceScheduler } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, IReader, ISettableObservable, ITransaction, autorunWithStore, derived, observableSignal, observableSignalFromEvent, observableValue, transaction, waitForState } from 'vs/base/common/observable';
import { IDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditorWidget2/diffProviderFactoryService';
import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { ISerializedLineRange, LineRange } from 'vs/editor/common/core/lineRange';
import { AdvancedLinesDiffComputer } from 'vs/editor/common/diff/advancedLinesDiffComputer';
import { IDocumentDiff, IDocumentDiffProvider } from 'vs/editor/common/diff/documentDiffProvider';
import { LineRangeMapping, MovedText } from 'vs/editor/common/diff/linesDiffComputer';
import { DefaultLinesDiffComputer } from 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer';
import { IDocumentDiff } from 'vs/editor/common/diff/documentDiffProvider';
import { MovedText } from 'vs/editor/common/diff/linesDiffComputer';
import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { IDiffEditorModel, IDiffEditorViewModel } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model';
import { TextEditInfo } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper';
@@ -19,19 +22,18 @@ import { combineTextEditInfos } from 'vs/editor/common/model/bracketPairsTextMod
import { DiffEditorOptions } from './diffEditorOptions';
export class DiffEditorViewModel extends Disposable implements IDiffEditorViewModel {
private readonly _isDiffUpToDate = observableValue<boolean>('isDiffUpToDate', false);
private readonly _isDiffUpToDate = observableValue<boolean>(this, false);
public readonly isDiffUpToDate: IObservable<boolean> = this._isDiffUpToDate;
private _lastDiff: IDocumentDiff | undefined;
private readonly _diff = observableValue<DiffState | undefined>('diff', undefined);
private readonly _diff = observableValue<DiffState | undefined>(this, undefined);
public readonly diff: IObservable<DiffState | undefined> = this._diff;
private readonly _unchangedRegions = observableValue<{ regions: UnchangedRegion[]; originalDecorationIds: string[]; modifiedDecorationIds: string[] }>(
'unchangedRegion',
this,
{ regions: [], originalDecorationIds: [], modifiedDecorationIds: [] }
);
public readonly unchangedRegions: IObservable<UnchangedRegion[]> = derived(r => {
/** @description unchangedRegions */
public readonly unchangedRegions: IObservable<UnchangedRegion[]> = derived(this, r => {
if (this._options.hideUnchangedRegions.read(r)) {
return this._unchangedRegions.read(r).regions;
} else {
@@ -46,13 +48,13 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
}
);
public readonly movedTextToCompare = observableValue<MovedText | undefined>('movedTextToCompare', undefined);
public readonly movedTextToCompare = observableValue<MovedText | undefined>(this, undefined);
private readonly _activeMovedText = observableValue<MovedText | undefined>('activeMovedText', undefined);
private readonly _hoveredMovedText = observableValue<MovedText | undefined>('hoveredMovedText', undefined);
private readonly _activeMovedText = observableValue<MovedText | undefined>(this, undefined);
private readonly _hoveredMovedText = observableValue<MovedText | undefined>(this, undefined);
public readonly activeMovedText = derived(r => this.movedTextToCompare.read(r) ?? this._hoveredMovedText.read(r) ?? this._activeMovedText.read(r));
public readonly activeMovedText = derived(this, r => this.movedTextToCompare.read(r) ?? this._hoveredMovedText.read(r) ?? this._activeMovedText.read(r));
public setActiveMovedText(movedText: MovedText | undefined): void {
this._activeMovedText.set(movedText, undefined);
@@ -64,10 +66,22 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
private readonly _cancellationTokenSource = new CancellationTokenSource();
private readonly _diffProvider = derived(this, reader => {
const diffProvider = this._diffProviderFactoryService.createDiffProvider(this._editor, {
diffAlgorithm: this._options.diffAlgorithm.read(reader)
});
const onChangeSignal = observableSignalFromEvent('onDidChange', diffProvider.onDidChange);
return {
diffProvider,
onChangeSignal,
};
});
constructor(
public readonly model: IDiffEditorModel,
private readonly _options: DiffEditorOptions,
documentDiffProvider: IDocumentDiffProvider,
private readonly _editor: IDiffEditor,
@IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService,
) {
super();
@@ -81,7 +95,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
result.changes,
model.original.getLineCount(),
model.modified.getLineCount(),
this._options.hideUnchangedRegionsminimumLineCount.read(reader),
this._options.hideUnchangedRegionsMinimumLineCount.read(reader),
this._options.hideUnchangedRegionsContextLineCount.read(reader),
);
@@ -98,18 +112,18 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
const originalDecorationIds = model.original.deltaDecorations(
lastUnchangedRegions.originalDecorationIds,
newUnchangedRegions.map(r => ({ range: r.originalRange.toInclusiveRange()!, options: { description: 'unchanged' } }))
newUnchangedRegions.map(r => ({ range: r.originalUnchangedRange.toInclusiveRange()!, options: { description: 'unchanged' } }))
);
const modifiedDecorationIds = model.modified.deltaDecorations(
lastUnchangedRegions.modifiedDecorationIds,
newUnchangedRegions.map(r => ({ range: r.modifiedRange.toInclusiveRange()!, options: { description: 'unchanged' } }))
newUnchangedRegions.map(r => ({ range: r.modifiedUnchangedRange.toInclusiveRange()!, options: { description: 'unchanged' } }))
);
for (const r of newUnchangedRegions) {
for (let i = 0; i < lastUnchangedRegions.regions.length; i++) {
if (r.originalRange.intersectsStrict(lastUnchangedRegionsOrigRanges[i])
&& r.modifiedRange.intersectsStrict(lastUnchangedRegionsModRanges[i])) {
if (r.originalUnchangedRange.intersectsStrict(lastUnchangedRegionsOrigRanges[i])
&& r.modifiedUnchangedRange.intersectsStrict(lastUnchangedRegionsModRanges[i])) {
r.setHiddenModifiedRange(lastUnchangedRegions.regions[i].getHiddenModifiedRange(undefined), tx);
break;
}
@@ -162,19 +176,19 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
debouncer.schedule();
}));
const documentDiffProviderOptionChanged = observableSignalFromEvent('documentDiffProviderOptionChanged', documentDiffProvider.onDidChange);
this._register(autorunWithStore(async (reader, store) => {
/** @description compute diff */
// So that they get recomputed when these settings change
this._options.hideUnchangedRegionsminimumLineCount.read(reader);
this._options.hideUnchangedRegionsMinimumLineCount.read(reader);
this._options.hideUnchangedRegionsContextLineCount.read(reader);
debouncer.cancel();
contentChangedSignal.read(reader);
documentDiffProviderOptionChanged.read(reader);
readHotReloadableExport(AdvancedLinesDiffComputer, reader);
const documentDiffProvider = this._diffProvider.read(reader);
documentDiffProvider.onChangeSignal.read(reader);
readHotReloadableExport(DefaultLinesDiffComputer, reader);
this._isDiffUpToDate.set(false, undefined);
@@ -190,7 +204,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
modifiedTextEditInfos = combineTextEditInfos(modifiedTextEditInfos, edits);
}));
let result = await documentDiffProvider.computeDiff(model.original, model.modified, {
let result = await documentDiffProvider.diffProvider.computeDiff(model.original, model.modified, {
ignoreTrimWhitespace: this._options.ignoreTrimWhitespace.read(reader),
maxComputationTimeMs: this._options.maxComputationTimeMs.read(reader),
computeMoves: this._options.showMoves.read(reader),
@@ -259,7 +273,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo
transaction(tx => {
for (const r of regions.regions) {
for (const range of ranges) {
if (r.modifiedRange.intersect(range)) {
if (r.modifiedUnchangedRange.intersect(range)) {
r.setHiddenModifiedRange(range, tx);
break;
}
@@ -293,7 +307,7 @@ export class DiffState {
export class DiffMapping {
constructor(
readonly lineRangeMapping: LineRangeMapping,
readonly lineRangeMapping: DetailedLineRangeMapping,
) {
/*
readonly movedTo: MovedText | undefined,
@@ -318,19 +332,19 @@ export class DiffMapping {
export class UnchangedRegion {
public static fromDiffs(
changes: readonly LineRangeMapping[],
changes: readonly DetailedLineRangeMapping[],
originalLineCount: number,
modifiedLineCount: number,
minHiddenLineCount: number,
minContext: number,
): UnchangedRegion[] {
const inversedMappings = LineRangeMapping.inverse(changes, originalLineCount, modifiedLineCount);
const inversedMappings = DetailedLineRangeMapping.inverse(changes, originalLineCount, modifiedLineCount);
const result: UnchangedRegion[] = [];
for (const mapping of inversedMappings) {
let origStart = mapping.originalRange.startLineNumber;
let modStart = mapping.modifiedRange.startLineNumber;
let length = mapping.originalRange.length;
let origStart = mapping.original.startLineNumber;
let modStart = mapping.modified.startLineNumber;
let length = mapping.original.length;
const atStart = origStart === 1 && modStart === 1;
const atEnd = origStart + length === originalLineCount + 1 && modStart + length === modifiedLineCount + 1;
@@ -356,23 +370,24 @@ export class UnchangedRegion {
return result;
}
public get originalRange(): LineRange {
public get originalUnchangedRange(): LineRange {
return LineRange.ofLength(this.originalLineNumber, this.lineCount);
}
public get modifiedRange(): LineRange {
public get modifiedUnchangedRange(): LineRange {
return LineRange.ofLength(this.modifiedLineNumber, this.lineCount);
}
private readonly _visibleLineCountTop = observableValue<number>('visibleLineCountTop', 0);
private readonly _visibleLineCountTop = observableValue<number>(this, 0);
public readonly visibleLineCountTop: ISettableObservable<number> = this._visibleLineCountTop;
private readonly _visibleLineCountBottom = observableValue<number>('visibleLineCountBottom', 0);
private readonly _visibleLineCountBottom = observableValue<number>(this, 0);
public readonly visibleLineCountBottom: ISettableObservable<number> = this._visibleLineCountBottom;
private readonly _shouldHideControls = derived(reader => /** @description isVisible */ this.visibleLineCountTop.read(reader) + this.visibleLineCountBottom.read(reader) === this.lineCount && !this.isDragged.read(reader));
private readonly _shouldHideControls = derived(this, reader => /** @description isVisible */
this.visibleLineCountTop.read(reader) + this.visibleLineCountBottom.read(reader) === this.lineCount && !this.isDragged.read(reader));
public readonly isDragged = observableValue<boolean>('isDragged', false);
public readonly isDragged = observableValue<boolean>(this, false);
constructor(
public readonly originalLineNumber: number,
@@ -4,33 +4,32 @@
*--------------------------------------------------------------------------------------------*/
import { $, h } from 'vs/base/browser/dom';
import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash';
import { findLast } from 'vs/base/common/arrays';
import { findLast } from 'vs/base/common/arraysFind';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Event } from 'vs/base/common/event';
import { IObservable, autorun, autorunWithStore, derived, derivedWithStore, disposableObservableValue, keepAlive, observableValue, transaction } from 'vs/base/common/observable';
import { toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, autorun, autorunWithStore, derived, derivedWithStore, disposableObservableValue, recomputeInitiallyAndOnChange, observableValue, transaction } from 'vs/base/common/observable';
import 'vs/css!./style';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions, IMouseTargetViewZone } from 'vs/editor/browser/editorBrowser';
import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from 'vs/editor/browser/editorExtensions';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditorWidget';
import { AccessibleDiffViewer } from 'vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer';
import { DiffEditorDecorations } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations';
import { DiffEditorSash } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorSash';
import { HideUnchangedRegionsFeature } from 'vs/editor/browser/widget/diffEditorWidget2/hideUnchangedRegionsFeature';
import { ViewZoneManager } from 'vs/editor/browser/widget/diffEditorWidget2/lineAlignment';
import { MovedBlocksLinesPart } from 'vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines';
import { OverviewRulerPart } from 'vs/editor/browser/widget/diffEditorWidget2/overviewRulerPart';
import { UnchangedRangesFeature } from 'vs/editor/browser/widget/diffEditorWidget2/unchangedRanges';
import { CSSStyle, ObservableElementSizeObserver, applyStyle, readHotReloadableExport } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider';
import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IDimension } from 'vs/editor/common/core/dimension';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { CursorChangeReason } from 'vs/editor/common/cursorEvents';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/legacyLinesDiffComputer';
import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { EditorType, IDiffEditorModel, IDiffEditorViewModel, IDiffEditorViewState } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
@@ -39,15 +38,21 @@ import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioC
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IEditorProgressService } from 'vs/platform/progress/common/progress';
import './colors';
import { DelegatingEditor } from './delegatingEditorImpl';
import { DiffEditorEditors } from './diffEditorEditors';
import { DiffEditorOptions } from './diffEditorOptions';
import { DiffEditorViewModel, DiffMapping, DiffState } from './diffEditorViewModel';
import { toDisposable } from 'vs/base/common/lifecycle';
import { IEditorProgressService } from 'vs/platform/progress/common/progress';
export interface IDiffCodeEditorWidgetOptions {
originalEditor?: ICodeEditorWidgetOptions;
modifiedEditor?: ICodeEditorWidgetOptions;
}
export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
public static ENTIRE_DIFF_OVERVIEW_WIDTH = OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH;
private readonly elements = h('div.monaco-diff-editor.side-by-side', { style: { position: 'relative', height: '100%' } }, [
h('div.noModificationsOverlay@overlay', { style: { position: 'absolute', height: '100%', visibility: 'hidden', } }, [$('span', {}, 'No Changes')]),
h('div.editor.original@original', { style: { position: 'absolute', height: '100%' } }),
@@ -66,13 +71,13 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
private readonly _rootSizeObserver: ObservableElementSizeObserver;
private readonly _sash: IObservable<DiffEditorSash | undefined>;
private readonly _boundarySashes = observableValue<IBoundarySashes | undefined>('boundarySashes', undefined);
private readonly _boundarySashes = observableValue<IBoundarySashes | undefined>(this, undefined);
private unchangedRangesFeature!: UnchangedRangesFeature;
private unchangedRangesFeature!: HideUnchangedRegionsFeature;
private _accessibleDiffViewerShouldBeVisible = observableValue('accessibleDiffViewerShouldBeVisible', false);
private _accessibleDiffViewerVisible = derived(reader =>
/** @description accessibleDiffViewerVisible */ this._options.onlyShowAccessibleDiffViewer.read(reader)
private _accessibleDiffViewerShouldBeVisible = observableValue(this, false);
private _accessibleDiffViewerVisible = derived(this, reader =>
this._options.onlyShowAccessibleDiffViewer.read(reader)
? true
: this._accessibleDiffViewerShouldBeVisible.read(reader)
);
@@ -80,7 +85,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
private readonly _options: DiffEditorOptions;
private readonly _editors: DiffEditorEditors;
private readonly movedBlocksLinesPart = observableValue<MovedBlocksLinesPart | undefined>('MovedBlocksLinesPart', undefined);
private readonly movedBlocksLinesPart = observableValue<MovedBlocksLinesPart | undefined>(this, undefined);
public get collapseUnchangedRegions() { return this._options.hideUnchangedRegions.get(); }
@@ -136,7 +141,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
(i, c, o, o2) => this._createInnerEditor(i, c, o, o2)
));
this._sash = derivedWithStore('sash', (reader, store) => {
this._sash = derivedWithStore(this, (reader, store) => {
const showSash = this._options.renderSideBySide.read(reader);
this.elements.root.classList.toggle('side-by-side', showSash);
if (!showSash) { return undefined; }
@@ -157,12 +162,12 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
}));
return result;
});
this._register(keepAlive(this._sash, true));
this._register(recomputeInitiallyAndOnChange(this._sash));
this._register(autorunWithStore((reader, store) => {
/** @description UnchangedRangesFeature */
this.unchangedRangesFeature = store.add(
this._instantiationService.createInstance(readHotReloadableExport(UnchangedRangesFeature, reader), this._editors, this._diffModel, this._options)
this._instantiationService.createInstance(readHotReloadableExport(HideUnchangedRegionsFeature, reader), this._editors, this._diffModel, this._options)
);
}));
@@ -218,7 +223,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
codeEditorService.addDiffEditor(this);
this._register(keepAlive(this._layoutInfo, true));
this._register(recomputeInitiallyAndOnChange(this._layoutInfo));
this._register(autorunWithStore((reader, store) => {
this.movedBlocksLinesPart.set(store.add(new (readHotReloadableExport(MovedBlocksLinesPart, reader))(
@@ -248,8 +253,8 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
const diffs = model.diff.get()?.mappings;
if (!diffs) { return; }
const diff = diffs.find(d =>
viewZone?.detail.afterLineNumber === d.lineRangeMapping.modifiedRange.startLineNumber - 1 ||
d.lineRangeMapping.modifiedRange.startLineNumber === lineNumber
viewZone?.detail.afterLineNumber === d.lineRangeMapping.modified.startLineNumber - 1 ||
d.lineRangeMapping.modified.startLineNumber === lineNumber
);
if (!diff) { return; }
this.revert(diff.lineRangeMapping);
@@ -260,10 +265,10 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
this._register(Event.runAndSubscribe(this._editors.modified.onDidChangeCursorPosition, (e) => {
if (e?.reason === CursorChangeReason.Explicit) {
const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.modifiedRange.contains(e.position.lineNumber));
if (diff?.lineRangeMapping.modifiedRange.isEmpty) {
const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.modified.contains(e.position.lineNumber));
if (diff?.lineRangeMapping.modified.isEmpty) {
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' });
} else if (diff?.lineRangeMapping.originalRange.isEmpty) {
} else if (diff?.lineRangeMapping.original.isEmpty) {
this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' });
} else if (diff) {
this._audioCueService.playAudioCue(AudioCue.diffLineModified, { source: 'diffEditor.cursorPositionChanged' });
@@ -280,6 +285,10 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
}));
}
public getViewWidth(): number {
return this._rootSizeObserver.width.get();
}
public getContentHeight() {
return this._editors.modified.getContentHeight();
}
@@ -289,8 +298,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
return editor;
}
private readonly _layoutInfo = derived(reader => {
/** @description modifiedEditorLayoutInfo */
private readonly _layoutInfo = derived(this, reader => {
const width = this._rootSizeObserver.width.read(reader);
const height = this._rootSizeObserver.height.read(reader);
const sashLeft = this._sash.read(reader)?.sashLeft.read(reader);
@@ -367,12 +375,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
}
public createViewModel(model: IDiffEditorModel): IDiffEditorViewModel {
return new DiffEditorViewModel(
model,
this._options,
// TODO@hediet make diffAlgorithm observable
this._instantiationService.createInstance(WorkerBasedDocumentDiffProvider, { diffAlgorithm: this._options.diffAlgorithm.get() })
);
return this._instantiationService.createInstance(DiffEditorViewModel, model, this._options, this);
}
override getModel(): IDiffEditorModel | null { return this._diffModel.get()?.model ?? null; }
@@ -436,7 +439,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
};
}
revert(diff: LineRangeMapping): void {
revert(diff: DetailedLineRangeMapping): void {
const model = this._diffModel.get()?.model;
if (!model) { return; }
@@ -447,8 +450,8 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
}))
: [
{
range: diff.modifiedRange.toExclusiveRange(),
text: model.original.getValueInRange(diff.originalRange.toExclusiveRange())
range: diff.modified.toExclusiveRange(),
text: model.original.getValueInRange(diff.original.toExclusiveRange())
}
];
@@ -456,8 +459,8 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
}
private _goTo(diff: DiffMapping): void {
this._editors.modified.setPosition(new Position(diff.lineRangeMapping.modifiedRange.startLineNumber, 1));
this._editors.modified.revealRangeInCenter(diff.lineRangeMapping.modifiedRange.toExclusiveRange());
this._editors.modified.setPosition(new Position(diff.lineRangeMapping.modified.startLineNumber, 1));
this._editors.modified.revealRangeInCenter(diff.lineRangeMapping.modified.toExclusiveRange());
}
goToDiff(target: 'previous' | 'next'): void {
@@ -470,15 +473,15 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
let diff: DiffMapping | undefined;
if (target === 'next') {
diff = diffs.find(d => d.lineRangeMapping.modifiedRange.startLineNumber > curLineNumber) ?? diffs[0];
diff = diffs.find(d => d.lineRangeMapping.modified.startLineNumber > curLineNumber) ?? diffs[0];
} else {
diff = findLast(diffs, d => d.lineRangeMapping.modifiedRange.startLineNumber < curLineNumber) ?? diffs[diffs.length - 1];
diff = findLast(diffs, d => d.lineRangeMapping.modified.startLineNumber < curLineNumber) ?? diffs[diffs.length - 1];
}
this._goTo(diff);
if (diff.lineRangeMapping.modifiedRange.isEmpty) {
if (diff.lineRangeMapping.modified.isEmpty) {
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { source: 'diffEditor.goToDiff' });
} else if (diff.lineRangeMapping.originalRange.isEmpty) {
} else if (diff.lineRangeMapping.original.isEmpty) {
this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { source: 'diffEditor.goToDiff' });
} else if (diff) {
this._audioCueService.playAudioCue(AudioCue.diffLineModified, { source: 'diffEditor.goToDiff' });
@@ -564,26 +567,26 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
}
}
function translatePosition(posInOriginal: Position, mappings: LineRangeMapping[]): Range {
const mapping = findLast(mappings, m => m.originalRange.startLineNumber <= posInOriginal.lineNumber);
function translatePosition(posInOriginal: Position, mappings: DetailedLineRangeMapping[]): Range {
const mapping = findLast(mappings, m => m.original.startLineNumber <= posInOriginal.lineNumber);
if (!mapping) {
// No changes before the position
return Range.fromPositions(posInOriginal);
}
if (mapping.originalRange.endLineNumberExclusive <= posInOriginal.lineNumber) {
const newLineNumber = posInOriginal.lineNumber - mapping.originalRange.endLineNumberExclusive + mapping.modifiedRange.endLineNumberExclusive;
if (mapping.original.endLineNumberExclusive <= posInOriginal.lineNumber) {
const newLineNumber = posInOriginal.lineNumber - mapping.original.endLineNumberExclusive + mapping.modified.endLineNumberExclusive;
return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));
}
if (!mapping.innerChanges) {
// Only for legacy algorithm
return Range.fromPositions(new Position(mapping.modifiedRange.startLineNumber, 1));
return Range.fromPositions(new Position(mapping.modified.startLineNumber, 1));
}
const innerMapping = findLast(mapping.innerChanges, m => m.originalRange.getStartPosition().isBeforeOrEqual(posInOriginal));
if (!innerMapping) {
const newLineNumber = posInOriginal.lineNumber - mapping.originalRange.startLineNumber + mapping.modifiedRange.startLineNumber;
const newLineNumber = posInOriginal.lineNumber - mapping.original.startLineNumber + mapping.modified.startLineNumber;
return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));
}
@@ -620,24 +623,24 @@ function toLineChanges(state: DiffState): ILineChange[] {
let modifiedEndLineNumber: number;
let innerChanges = m.innerChanges;
if (m.originalRange.isEmpty) {
if (m.original.isEmpty) {
// Insertion
originalStartLineNumber = m.originalRange.startLineNumber - 1;
originalStartLineNumber = m.original.startLineNumber - 1;
originalEndLineNumber = 0;
innerChanges = undefined;
} else {
originalStartLineNumber = m.originalRange.startLineNumber;
originalEndLineNumber = m.originalRange.endLineNumberExclusive - 1;
originalStartLineNumber = m.original.startLineNumber;
originalEndLineNumber = m.original.endLineNumberExclusive - 1;
}
if (m.modifiedRange.isEmpty) {
if (m.modified.isEmpty) {
// Deletion
modifiedStartLineNumber = m.modifiedRange.startLineNumber - 1;
modifiedStartLineNumber = m.modified.startLineNumber - 1;
modifiedEndLineNumber = 0;
innerChanges = undefined;
} else {
modifiedStartLineNumber = m.modifiedRange.startLineNumber;
modifiedEndLineNumber = m.modifiedRange.endLineNumberExclusive - 1;
modifiedStartLineNumber = m.modified.startLineNumber;
modifiedEndLineNumber = m.modified.endLineNumberExclusive - 1;
}
return {
@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDiffEditor } from 'vs/editor/browser/editorBrowser';
import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider';
import { IDocumentDiffProvider } from 'vs/editor/common/diff/documentDiffProvider';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IDiffProviderFactoryService = createDecorator<IDiffProviderFactoryService>('diffProviderFactoryService');
export interface IDocumentDiffProviderOptions {
readonly diffAlgorithm?: 'legacy' | 'advanced';
}
export interface IDiffProviderFactoryService {
readonly _serviceBrand: undefined;
createDiffProvider(editor: IDiffEditor, options: IDocumentDiffProviderOptions): IDocumentDiffProvider;
}
export class DiffProviderFactoryService implements IDiffProviderFactoryService {
readonly _serviceBrand: undefined;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
) { }
createDiffProvider(editor: IDiffEditor, options: IDocumentDiffProviderOptions): IDocumentDiffProvider {
return this.instantiationService.createInstance(WorkerBasedDocumentDiffProvider, options);
}
}
registerSingleton(IDiffProviderFactoryService, DiffProviderFactoryService, InstantiationType.Delayed);
@@ -6,7 +6,6 @@
import { $, addDisposableListener, h, reset } from 'vs/base/browser/dom';
import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { compareBy, numberComparator, reverseOrder } from 'vs/base/common/arrays';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Codicon } from 'vs/base/common/codicons';
import { Event } from 'vs/base/common/event';
import { MarkdownString } from 'vs/base/common/htmlContent';
@@ -19,7 +18,7 @@ import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditorWidget2/di
import { DiffEditorOptions } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorOptions';
import { DiffEditorViewModel, UnchangedRegion } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel';
import { OutlineModel } from 'vs/editor/browser/widget/diffEditorWidget2/outlineModel';
import { PlaceholderViewZone, ViewZoneOverlayWidget, applyObservableDecorations, applyStyle, applyViewZones } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { DisposableCancellationTokenSource, PlaceholderViewZone, ViewZoneOverlayWidget, applyObservableDecorations, applyStyle, applyViewZones } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Position } from 'vs/editor/common/core/position';
@@ -30,14 +29,12 @@ import { IModelDecorationOptions, IModelDeltaDecoration, ITextModel } from 'vs/e
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { localize } from 'vs/nls';
export class UnchangedRangesFeature extends Disposable {
export class HideUnchangedRegionsFeature extends Disposable {
private _isUpdatingViewZones = false;
public get isUpdatingViewZones(): boolean { return this._isUpdatingViewZones; }
private readonly _modifiedModel = observableFromEvent(this._editors.modified.onDidChangeModel, () => this._editors.modified.getModel());
private readonly _modifiedOutlineSource = derivedWithStore('modified outline source', (reader, store) => {
const m = this._modifiedModel.read(reader);
private readonly _modifiedOutlineSource = derivedWithStore(this, (reader, store) => {
const m = this._editors.modifiedModel.read(reader);
if (!m) { return undefined; }
return store.add(new OutlineSource(this._languageFeaturesService, m));
});
@@ -76,14 +73,15 @@ export class UnchangedRangesFeature extends Disposable {
const unchangedRegions = this._diffModel.map((m, reader) => m?.diff.read(reader)?.mappings.length === 0 ? [] : m?.unchangedRegions.read(reader) ?? []);
const viewZones = derivedWithStore('view zones', (reader, store) => {
const viewZones = derivedWithStore(this, (reader, store) => {
/** @description view Zones */
const modifiedOutlineSource = this._modifiedOutlineSource.read(reader);
if (!modifiedOutlineSource) { return { origViewZones: [], modViewZones: [] }; }
const origViewZones: IViewZone[] = [];
const modViewZones: IViewZone[] = [];
const sideBySide = this._options.renderSideBySide.read(reader);
const modifiedOutlineSource = this._modifiedOutlineSource.read(reader);
if (!modifiedOutlineSource) { return { origViewZones, modViewZones }; }
const curUnchangedRegions = unchangedRegions.read(reader);
for (const r of curUnchangedRegions) {
if (r.shouldHideControls(reader)) {
@@ -94,13 +92,30 @@ export class UnchangedRangesFeature extends Disposable {
const d = derived(reader => /** @description hiddenOriginalRangeStart */ r.getHiddenOriginalRange(reader).startLineNumber - 1);
const origVz = new PlaceholderViewZone(d, 24);
origViewZones.push(origVz);
store.add(new CollapsedCodeOverlayWidget(this._editors.original, origVz, r, r.originalRange, !sideBySide, modifiedOutlineSource, l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, undefined), this._options));
store.add(new CollapsedCodeOverlayWidget(
this._editors.original,
origVz,
r,
r.originalUnchangedRange,
!sideBySide,
modifiedOutlineSource,
l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, undefined),
this._options
));
}
{
const d = derived(reader => /** @description hiddenModifiedRangeStart */ r.getHiddenModifiedRange(reader).startLineNumber - 1);
const modViewZone = new PlaceholderViewZone(d, 24);
modViewZones.push(modViewZone);
store.add(new CollapsedCodeOverlayWidget(this._editors.modified, modViewZone, r, r.modifiedRange, false, modifiedOutlineSource, l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, undefined), this._options));
store.add(new CollapsedCodeOverlayWidget(
this._editors.modified,
modViewZone,
r,
r.modifiedUnchangedRange,
false,
modifiedOutlineSource,
l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, undefined), this._options
));
}
}
@@ -125,14 +140,14 @@ export class UnchangedRangesFeature extends Disposable {
/** @description decorations */
const curUnchangedRegions = unchangedRegions.read(reader);
const result = curUnchangedRegions.map<IModelDeltaDecoration>(r => ({
range: r.originalRange.toInclusiveRange()!,
range: r.originalUnchangedRange.toInclusiveRange()!,
options: unchangedLinesDecoration,
}));
for (const r of curUnchangedRegions) {
if (r.shouldHideControls(reader)) {
result.push({
range: Range.fromPositions(new Position(r.originalLineNumber, 1)),
options: unchangedLinesDecorationShow
options: unchangedLinesDecorationShow,
});
}
}
@@ -143,14 +158,14 @@ export class UnchangedRangesFeature extends Disposable {
/** @description decorations */
const curUnchangedRegions = unchangedRegions.read(reader);
const result = curUnchangedRegions.map<IModelDeltaDecoration>(r => ({
range: r.modifiedRange.toInclusiveRange()!,
range: r.modifiedUnchangedRange.toInclusiveRange()!,
options: unchangedLinesDecoration,
}));
for (const r of curUnchangedRegions) {
if (r.shouldHideControls(reader)) {
result.push({
range: LineRange.ofLength(r.modifiedLineNumber, 1).toInclusiveRange()!,
options: unchangedLinesDecorationShow
options: unchangedLinesDecorationShow,
});
}
}
@@ -172,7 +187,7 @@ export class UnchangedRangesFeature extends Disposable {
const lineNumber = event.target.position.lineNumber;
const model = this._diffModel.get();
if (!model) { return; }
const region = model.unchangedRegions.get().find(r => r.modifiedRange.includes(lineNumber));
const region = model.unchangedRegions.get().find(r => r.modifiedUnchangedRange.includes(lineNumber));
if (!region) { return; }
region.collapseAll(undefined);
event.event.stopPropagation();
@@ -185,7 +200,7 @@ export class UnchangedRangesFeature extends Disposable {
const lineNumber = event.target.position.lineNumber;
const model = this._diffModel.get();
if (!model) { return; }
const region = model.unchangedRegions.get().find(r => r.originalRange.includes(lineNumber));
const region = model.unchangedRegions.get().find(r => r.originalUnchangedRange.includes(lineNumber));
if (!region) { return; }
region.collapseAll(undefined);
event.event.stopPropagation();
@@ -195,14 +210,8 @@ export class UnchangedRangesFeature extends Disposable {
}
}
class DisposableCancellationTokenSource extends CancellationTokenSource {
public override dispose() {
super.dispose(true);
}
}
class OutlineSource extends Disposable {
private readonly _currentModel = observableValue<OutlineModel | undefined>('current model', undefined);
private readonly _currentModel = observableValue<OutlineModel | undefined>(this, undefined);
constructor(
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@@ -251,7 +260,8 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget {
h('div.top@top', { title: localize('diff.hiddenLines.top', 'Click or drag to show more above') }),
h('div.center@content', { style: { display: 'flex' } }, [
h('div@first', { style: { display: 'flex', justifyContent: 'center', alignItems: 'center', flexShrink: '0' } },
[$('a', { title: localize('showAll', 'Show all'), role: 'button', onclick: () => { this.showAll(); } }, ...renderLabelWithIcons('$(unfold)'))]
[$('a', { title: localize('showAll', 'Show all'), role: 'button', onclick: () => { this._unchangedRegion.showAll(undefined); } },
...renderLabelWithIcons('$(unfold)'))]
),
h('div@others', { style: { display: 'flex', justifyContent: 'center', alignItems: 'center' } }),
]),
@@ -370,7 +380,7 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget {
span.addEventListener('dblclick', e => {
if (e.button !== 0) { return; }
e.preventDefault();
this.showAll();
this._unchangedRegion.showAll(undefined);
});
children.push(span);
@@ -405,6 +415,4 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget {
reset(this._nodes.others, ...children);
}));
}
private showAll() { this._unchangedRegion.showAll(undefined); }
}
@@ -13,7 +13,7 @@ import { IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrow
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { EndOfLineSequence, ITextModel } from 'vs/editor/common/model';
import { localize } from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
@@ -39,7 +39,7 @@ export class InlineDiffDeletedCodeMargin extends Disposable {
private readonly _getViewZoneId: () => string,
private readonly _marginDomNode: HTMLElement,
private readonly _modifiedEditor: CodeEditorWidget,
private readonly _diff: LineRangeMapping,
private readonly _diff: DetailedLineRangeMapping,
private readonly _editor: DiffEditorWidget2,
private readonly _viewLineCounts: number[],
private readonly _originalTextModel: ITextModel,
@@ -70,38 +70,38 @@ export class InlineDiffDeletedCodeMargin extends Disposable {
getAnchor: () => ({ x, y }),
getActions: () => {
const actions: Action[] = [];
const isDeletion = _diff.modifiedRange.isEmpty;
const isDeletion = _diff.modified.isEmpty;
// default action
actions.push(new Action(
'diff.clipboard.copyDeletedContent',
isDeletion
? (_diff.originalRange.length > 1
? (_diff.original.length > 1
? localize('diff.clipboard.copyDeletedLinesContent.label', "Copy deleted lines")
: localize('diff.clipboard.copyDeletedLinesContent.single.label', "Copy deleted line"))
: (_diff.originalRange.length > 1
: (_diff.original.length > 1
? localize('diff.clipboard.copyChangedLinesContent.label', "Copy changed lines")
: localize('diff.clipboard.copyChangedLinesContent.single.label', "Copy changed line")),
undefined,
true,
async () => {
const originalText = this._originalTextModel.getValueInRange(_diff.originalRange.toExclusiveRange());
const originalText = this._originalTextModel.getValueInRange(_diff.original.toExclusiveRange());
await this._clipboardService.writeText(originalText);
}
));
if (_diff.originalRange.length > 1) {
if (_diff.original.length > 1) {
actions.push(new Action(
'diff.clipboard.copyDeletedLineContent',
isDeletion
? localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})",
_diff.originalRange.startLineNumber + currentLineNumberOffset)
_diff.original.startLineNumber + currentLineNumberOffset)
: localize('diff.clipboard.copyChangedLineContent.label', "Copy changed line ({0})",
_diff.originalRange.startLineNumber + currentLineNumberOffset),
_diff.original.startLineNumber + currentLineNumberOffset),
undefined,
true,
async () => {
let lineContent = this._originalTextModel.getLineContent(_diff.originalRange.startLineNumber + currentLineNumberOffset);
let lineContent = this._originalTextModel.getLineContent(_diff.original.startLineNumber + currentLineNumberOffset);
if (lineContent === '') {
// empty line -> new line
const eof = this._originalTextModel.getEndOfLineSequence();
@@ -25,7 +25,7 @@ import { animatedObservable, joinCombine } from 'vs/editor/browser/widget/diffEd
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Position } from 'vs/editor/common/core/position';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { BackgroundTokenizationState } from 'vs/editor/common/tokenizationTextModelPart';
import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel';
@@ -39,14 +39,14 @@ import { DiffEditorOptions } from './diffEditorOptions';
* Synchronizes scrolling.
*/
export class ViewZoneManager extends Disposable {
private readonly _originalTopPadding = observableValue('originalTopPadding', 0);
private readonly _originalTopPadding = observableValue(this, 0);
private readonly _originalScrollTop: IObservable<number>;
private readonly _originalScrollOffset = observableValue<number, boolean>('originalScrollOffset', 0);
private readonly _originalScrollOffset = observableValue<number, boolean>(this, 0);
private readonly _originalScrollOffsetAnimated = animatedObservable(this._originalScrollOffset, this._store);
private readonly _modifiedTopPadding = observableValue('modifiedTopPadding', 0);
private readonly _modifiedTopPadding = observableValue(this, 0);
private readonly _modifiedScrollTop: IObservable<number>;
private readonly _modifiedScrollOffset = observableValue<number, boolean>('modifiedScrollOffset', 0);
private readonly _modifiedScrollOffset = observableValue<number, boolean>(this, 0);
private readonly _modifiedScrollOffsetAnimated = animatedObservable(this._modifiedScrollOffset, this._store);
constructor(
@@ -193,7 +193,7 @@ export class ViewZoneManager extends Disposable {
const decorations: InlineDecoration[] = [];
for (const i of a.diff.innerChanges || []) {
decorations.push(new InlineDecoration(
i.originalRange.delta(-(a.diff.originalRange.startLineNumber - 1)),
i.originalRange.delta(-(a.diff.original.startLineNumber - 1)),
diffDeleteDecoration.className!,
InlineDecorationType.Regular
));
@@ -287,7 +287,7 @@ export class ViewZoneManager extends Disposable {
}
let marginDomNode: HTMLElement | undefined = undefined;
if (a.diff && a.diff.modifiedRange.isEmpty && this._options.shouldRenderRevertArrows.read(reader)) {
if (a.diff && a.diff.modified.isEmpty && this._options.shouldRenderRevertArrows.read(reader)) {
marginDomNode = createViewZoneMarginArrow();
}
@@ -472,7 +472,7 @@ interface ILineRangeAlignment {
* If this range alignment is a direct result of a diff, then this is the diff's line mapping.
* Only used for inline-view.
*/
diff?: LineRangeMapping;
diff?: DetailedLineRangeMapping;
}
function computeRangeAlignment(
@@ -540,11 +540,11 @@ function computeRangeAlignment(
for (const m of diffs) {
const c = m.lineRangeMapping;
handleAlignmentsOutsideOfDiffs(c.originalRange.startLineNumber, c.modifiedRange.startLineNumber);
handleAlignmentsOutsideOfDiffs(c.original.startLineNumber, c.modified.startLineNumber);
let first = true;
let lastModLineNumber = c.modifiedRange.startLineNumber;
let lastOrigLineNumber = c.originalRange.startLineNumber;
let lastModLineNumber = c.modified.startLineNumber;
let lastOrigLineNumber = c.original.startLineNumber;
function emitAlignment(origLineNumberExclusive: number, modLineNumberExclusive: number) {
if (origLineNumberExclusive < lastOrigLineNumber || modLineNumberExclusive < lastModLineNumber) {
@@ -593,10 +593,10 @@ function computeRangeAlignment(
}
}
emitAlignment(c.originalRange.endLineNumberExclusive, c.modifiedRange.endLineNumberExclusive);
emitAlignment(c.original.endLineNumberExclusive, c.modified.endLineNumberExclusive);
lastOriginalLineNumber = c.originalRange.endLineNumberExclusive;
lastModifiedLineNumber = c.modifiedRange.endLineNumberExclusive;
lastOriginalLineNumber = c.original.endLineNumberExclusive;
lastModifiedLineNumber = c.modified.endLineNumberExclusive;
}
handleAlignmentsOutsideOfDiffs(Number.MAX_VALUE, Number.MAX_VALUE);
@@ -6,10 +6,11 @@
import { h } from 'vs/base/browser/dom';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
import { booleanComparator, compareBy, findMaxIdxBy, numberComparator, tieBreakComparators } from 'vs/base/common/arrays';
import { booleanComparator, compareBy, numberComparator, tieBreakComparators } from 'vs/base/common/arrays';
import { findMaxIdxBy } from 'vs/base/common/arraysFind';
import { Codicon } from 'vs/base/common/codicons';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, autorun, autorunHandleChanges, autorunWithStore, constObservable, derived, derivedWithStore, keepAlive, observableFromEvent, observableSignalFromEvent, observableValue } from 'vs/base/common/observable';
import { IObservable, autorun, autorunHandleChanges, autorunWithStore, constObservable, derived, derivedWithStore, observableFromEvent, observableSignalFromEvent, observableValue, recomputeInitiallyAndOnChange } from 'vs/base/common/observable';
import { ThemeIcon } from 'vs/base/common/themables';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors';
@@ -29,7 +30,7 @@ export class MovedBlocksLinesPart extends Disposable {
private readonly _modifiedScrollTop = observableFromEvent(this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop());
private readonly _viewZonesChanged = observableSignalFromEvent('onDidChangeViewZones', this._editors.modified.onDidChangeViewZones);
public readonly width = observableValue('width', 0);
public readonly width = observableValue(this, 0);
constructor(
private readonly _rootElement: HTMLElement,
@@ -58,7 +59,7 @@ export class MovedBlocksLinesPart extends Disposable {
this._element.style.width = `${info.verticalScrollbarWidth + info.contentLeft - MovedBlocksLinesPart.movedCodeBlockPadding + this.width.read(reader)}px`;
}));
this._register(keepAlive(this._state, true));
this._register(recomputeInitiallyAndOnChange(this._state));
const movedBlockViewZones = derived(reader => {
const model = this._diffModel.read(reader);
@@ -136,8 +137,8 @@ export class MovedBlocksLinesPart extends Disposable {
private readonly _modifiedViewZonesChangedSignal = observableSignalFromEvent('modified.onDidChangeViewZones', this._editors.modified.onDidChangeViewZones);
private readonly _originalViewZonesChangedSignal = observableSignalFromEvent('original.onDidChangeViewZones', this._editors.original.onDidChangeViewZones);
private readonly _state = derivedWithStore('state', (reader, store) => {
/** @description update moved blocks lines */
private readonly _state = derivedWithStore((reader, store) => {
/** @description state */
this._element.replaceChildren();
const model = this._diffModel.read(reader);
@@ -231,6 +231,7 @@ export class OutlineModel extends TreeElement {
return result._compact();
}
}).finally(() => {
cts.dispose();
listener.dispose();
});
}
@@ -127,8 +127,8 @@ export class OverviewRulerPart extends Disposable {
});
}
const originalZones = createZones((diff || []).map(d => d.lineRangeMapping.originalRange), colors.removeColor, this._editors.original);
const modifiedZones = createZones((diff || []).map(d => d.lineRangeMapping.modifiedRange), colors.insertColor, this._editors.modified);
const originalZones = createZones((diff || []).map(d => d.lineRangeMapping.original), colors.removeColor, this._editors.original);
const modifiedZones = createZones((diff || []).map(d => d.lineRangeMapping.modified), colors.insertColor, this._editors.modified);
originalOverviewRuler?.setZones(originalZones);
modifiedOverviewRuler?.setZones(modifiedZones);
}));
@@ -3,9 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes';
import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { diffEditorWidgetTtPolicy } from 'vs/editor/browser/widget/diffEditorWidget';
import { EditorFontLigatures, EditorOption, FindComputedEditorOptionValueById } from 'vs/editor/common/config/editorOptions';
import { FontInfo } from 'vs/editor/common/config/fontInfo';
import { StringBuilder } from 'vs/editor/common/core/stringBuilder';
@@ -15,7 +15,7 @@ import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations';
import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer';
import { InlineDecoration, ViewLineRenderingData } from 'vs/editor/common/viewModel';
const ttPolicy = diffEditorWidgetTtPolicy;
const ttPolicy = createTrustedTypesPolicy('diffEditorWidget', { createHTML: value => value });
export function renderLines(source: LineSource, options: RenderOptions, decorations: InlineDecoration[], domNode: HTMLElement): RenderLinesResult {
applyFontInfo(domNode, options.fontInfo);
@@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { IDimension } from 'vs/base/browser/dom';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { isHotReloadEnabled, registerHotReloadHandler } from 'vs/base/common/hotReload';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable';
import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver';
@@ -55,7 +57,7 @@ export function joinCombine<T>(arr1: readonly T[], arr2: readonly T[], keySelect
export function applyObservableDecorations(editor: ICodeEditor, decorations: IObservable<IModelDeltaDecoration[]>): IDisposable {
const d = new DisposableStore();
const decorationsCollection = editor.createDecorationsCollection();
d.add(autorunOpts({ debugName: `Apply decorations from ${decorations.debugName}` }, reader => {
d.add(autorunOpts({ debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => {
const d = decorations.read(reader);
decorationsCollection.set(d);
}));
@@ -98,8 +100,8 @@ export class ObservableElementSizeObserver extends Disposable {
super();
this.elementSizeObserver = this._register(new ElementSizeObserver(element, dimension));
this._width = observableValue('width', this.elementSizeObserver.getWidth());
this._height = observableValue('height', this.elementSizeObserver.getHeight());
this._width = observableValue(this, this.elementSizeObserver.getWidth());
this._height = observableValue(this, this.elementSizeObserver.getHeight());
this._register(this.elementSizeObserver.onDidChange(e => transaction(tx => {
/** @description Set width/height from elementSizeObserver */
@@ -212,8 +214,8 @@ export interface IObservableViewZone extends IViewZone {
export class PlaceholderViewZone implements IObservableViewZone {
public readonly domNode = document.createElement('div');
private readonly _actualTop = observableValue<number | undefined>('actualTop', undefined);
private readonly _actualHeight = observableValue<number | undefined>('actualHeight', undefined);
private readonly _actualTop = observableValue<number | undefined>(this, undefined);
private readonly _actualHeight = observableValue<number | undefined>(this, undefined);
public readonly actualTop: IObservable<number | undefined> = this._actualTop;
public readonly actualHeight: IObservable<number | undefined> = this._actualHeight;
@@ -294,28 +296,21 @@ export function readHotReloadableExport<T>(value: T, reader: IReader | undefined
}
export function observeHotReloadableExports(values: any[], reader: IReader | undefined): void {
const hotReload_deprecateExports = (globalThis as unknown as {
// This property it defined by the monaco editor playground server
$hotReload_deprecateExports: Set<(oldExports: Record<string, unknown>, newExports: Record<string, unknown>) => boolean>;
}).$hotReload_deprecateExports;
if (!hotReload_deprecateExports) {
return;
if (isHotReloadEnabled()) {
const o = observableSignalFromEvent(
'reload',
event => registerHotReloadHandler(oldExports => {
if (![...Object.values(oldExports)].some(v => values.includes(v))) {
return undefined;
}
return (_newExports) => {
event(undefined);
return true;
};
})
);
o.read(reader);
}
const o = observableSignalFromEvent('reload', e => {
function handleExports(oldExports: Record<string, unknown>, _newExports: Record<string, unknown>) {
if ([...Object.values(oldExports)].some(v => values.includes(v))) {
e(undefined);
return true;
}
return false;
}
hotReload_deprecateExports.add(handleExports);
return {
dispose() { hotReload_deprecateExports.delete(handleExports); }
};
});
o.read(reader);
}
export function applyViewZones(editor: ICodeEditor, viewZones: IObservable<IObservableViewZone[]>, setIsUpdating?: (isUpdatingViewZones: boolean) => void): IDisposable {
@@ -375,3 +370,9 @@ export function applyViewZones(editor: ICodeEditor, viewZones: IObservable<IObse
return store;
}
export class DisposableCancellationTokenSource extends CancellationTokenSource {
public override dispose() {
super.dispose(true);
}
}
@@ -1,278 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'vs/base/common/assert';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import * as objects from 'vs/base/common/objects';
import { IDiffEditor } from 'vs/editor/browser/editorBrowser';
import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents';
import { Range } from 'vs/editor/common/core/range';
import { ILineChange } from 'vs/editor/common/diff/legacyLinesDiffComputer';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
interface IDiffRange {
rhs: boolean;
range: Range;
}
export interface Options {
followsCaret?: boolean;
ignoreCharChanges?: boolean;
alwaysRevealFirst?: boolean;
findResultLoop?: boolean;
}
const defaultOptions: Options = {
followsCaret: true,
ignoreCharChanges: true,
alwaysRevealFirst: true,
findResultLoop: true
};
export interface IDiffNavigator {
canNavigate(): boolean;
next(): void;
previous(): void;
dispose(): void;
}
/**
* Create a new diff navigator for the provided diff editor.
*/
export class DiffNavigator extends Disposable implements IDiffNavigator {
private readonly _editor: IDiffEditor;
private readonly _options: Options;
private readonly _onDidUpdate = this._register(new Emitter<this>());
readonly onDidUpdate: Event<this> = this._onDidUpdate.event;
private disposed: boolean;
public revealFirst: boolean;
private nextIdx: number;
private ranges: IDiffRange[];
private ignoreSelectionChange: boolean;
constructor(
editor: IDiffEditor,
options: Options = {},
@IAudioCueService private readonly _audioCueService: IAudioCueService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService
) {
super();
this._editor = editor;
this._options = objects.mixin(options, defaultOptions, false);
this.disposed = false;
this.nextIdx = -1;
this.ranges = [];
this.ignoreSelectionChange = false;
this.revealFirst = Boolean(this._options.alwaysRevealFirst);
this._register(this._editor.onDidUpdateDiff(() => this._onDiffUpdated()));
if (this._options.followsCaret) {
this._register(this._editor.getModifiedEditor().onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => {
if (this.ignoreSelectionChange) {
return;
}
this._updateAccessibilityState(e.position.lineNumber);
this.nextIdx = -1;
}));
}
// init things
this._init();
}
private _init(): void {
const changes = this._editor.getLineChanges();
if (!changes) {
return;
}
}
private _onDiffUpdated(): void {
this._init();
this._compute(this._editor.getLineChanges());
if (this.revealFirst) {
// Only reveal first on first non-null changes
if (this._editor.getLineChanges() !== null) {
this.revealFirst = false;
this.nextIdx = -1;
this.next(ScrollType.Immediate);
}
}
}
private _compute(lineChanges: ILineChange[] | null): void {
// new ranges
this.ranges = [];
if (lineChanges) {
// create ranges from changes
lineChanges.forEach((lineChange) => {
if (!this._options.ignoreCharChanges && lineChange.charChanges) {
lineChange.charChanges.forEach((charChange) => {
this.ranges.push({
rhs: true,
range: new Range(
charChange.modifiedStartLineNumber,
charChange.modifiedStartColumn,
charChange.modifiedEndLineNumber,
charChange.modifiedEndColumn)
});
});
} else {
if (lineChange.modifiedEndLineNumber === 0) {
// a deletion
this.ranges.push({
rhs: true,
range: new Range(lineChange.modifiedStartLineNumber, 1, lineChange.modifiedStartLineNumber + 1, 1)
});
} else {
// an insertion or modification
this.ranges.push({
rhs: true,
range: new Range(lineChange.modifiedStartLineNumber, 1, lineChange.modifiedEndLineNumber + 1, 1)
});
}
}
});
}
// sort
this.ranges.sort((left, right) => Range.compareRangesUsingStarts(left.range, right.range));
this._onDidUpdate.fire(this);
}
private _initIdx(fwd: boolean): void {
let found = false;
const position = this._editor.getPosition();
if (!position) {
this.nextIdx = 0;
return;
}
for (let i = 0, len = this.ranges.length; i < len && !found; i++) {
const range = this.ranges[i].range;
if (position.isBeforeOrEqual(range.getStartPosition())) {
this.nextIdx = i + (fwd ? 0 : -1);
found = true;
}
}
if (!found) {
// after the last change
this.nextIdx = fwd ? 0 : this.ranges.length - 1;
}
if (this.nextIdx < 0) {
this.nextIdx = this.ranges.length - 1;
}
}
private _move(fwd: boolean, scrollType: ScrollType): void {
assert.ok(!this.disposed, 'Illegal State - diff navigator has been disposed');
if (!this.canNavigate()) {
return;
}
if (this.nextIdx === -1) {
this._initIdx(fwd);
} else if (fwd) {
this.nextIdx += 1;
if (this.nextIdx >= this.ranges.length) {
this.nextIdx = 0;
}
} else {
this.nextIdx -= 1;
if (this.nextIdx < 0) {
this.nextIdx = this.ranges.length - 1;
}
}
const info = this.ranges[this.nextIdx];
this.ignoreSelectionChange = true;
try {
const pos = info.range.getStartPosition();
this._editor.setPosition(pos);
this._editor.revealRangeInCenter(info.range, scrollType);
this._updateAccessibilityState(pos.lineNumber, true);
} finally {
this.ignoreSelectionChange = false;
}
}
_updateAccessibilityState(lineNumber: number, jumpToChange?: boolean): void {
const modifiedEditor = this._editor.getModel()?.modified;
if (!modifiedEditor) {
return;
}
const insertedOrModified = modifiedEditor.getLineDecorations(lineNumber).find(l => l.options.className === 'line-insert');
if (insertedOrModified) {
this._audioCueService.playAudioCue(AudioCue.diffLineModified, { allowManyInParallel: true });
} else if (jumpToChange) {
// The modified editor does not include deleted lines, but when
// we are moved to the area where lines were deleted, play this cue
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { allowManyInParallel: true });
} else {
return;
}
const codeEditor = this._codeEditorService.getActiveCodeEditor();
if (jumpToChange && codeEditor && insertedOrModified && this._accessibilityService.isScreenReaderOptimized()) {
codeEditor.setSelection({ startLineNumber: lineNumber, startColumn: 0, endLineNumber: lineNumber, endColumn: Number.MAX_VALUE });
codeEditor.writeScreenReaderContent('diff-navigation');
}
}
canNavigate(): boolean {
return this.ranges && this.ranges.length > 0;
}
next(scrollType: ScrollType = ScrollType.Smooth): void {
if (!this.canNavigateNext()) {
return;
}
this._move(true, scrollType);
}
previous(scrollType: ScrollType = ScrollType.Smooth): void {
if (!this.canNavigatePrevious()) {
return;
}
this._move(false, scrollType);
}
canNavigateNext(): boolean {
return this.canNavigateLoop() || this.nextIdx < this.ranges.length - 1;
}
canNavigatePrevious(): boolean {
return this.canNavigateLoop() || this.nextIdx !== 0;
}
canNavigateLoop(): boolean {
return Boolean(this._options.findResultLoop);
}
override dispose(): void {
super.dispose();
this.ranges = [];
this.disposed = true;
}
}
-826
View File
@@ -1,826 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { Action } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Disposable } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables';
import { Constants } from 'vs/base/common/uint';
import 'vs/css!./media/diffReview';
import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget';
import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { ILineChange } from 'vs/editor/common/diff/legacyLinesDiffComputer';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { ILanguageIdCodec } from 'vs/editor/common/languages';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model';
import { LineTokens } from 'vs/editor/common/tokens/lineTokens';
import { RenderLineInput, renderViewLine2 as renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer';
import { ViewLineRenderingData } from 'vs/editor/common/viewModel';
import * as nls from 'vs/nls';
import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
const DIFF_LINES_PADDING = 3;
const enum DiffEntryType {
Equal = 0,
Insert = 1,
Delete = 2
}
class DiffEntry {
readonly originalLineStart: number;
readonly originalLineEnd: number;
readonly modifiedLineStart: number;
readonly modifiedLineEnd: number;
constructor(originalLineStart: number, originalLineEnd: number, modifiedLineStart: number, modifiedLineEnd: number) {
this.originalLineStart = originalLineStart;
this.originalLineEnd = originalLineEnd;
this.modifiedLineStart = modifiedLineStart;
this.modifiedLineEnd = modifiedLineEnd;
}
public getType(): DiffEntryType {
if (this.originalLineStart === 0) {
return DiffEntryType.Insert;
}
if (this.modifiedLineStart === 0) {
return DiffEntryType.Delete;
}
return DiffEntryType.Equal;
}
}
const enum DiffEditorLineClasses {
Insert = 'line-insert',
Delete = 'line-delete'
}
class Diff {
readonly entries: DiffEntry[];
constructor(entries: DiffEntry[]) {
this.entries = entries;
}
}
const diffReviewInsertIcon = registerIcon('diff-review-insert', Codicon.add, nls.localize('diffReviewInsertIcon', 'Icon for \'Insert\' in diff review.'));
const diffReviewRemoveIcon = registerIcon('diff-review-remove', Codicon.remove, nls.localize('diffReviewRemoveIcon', 'Icon for \'Remove\' in diff review.'));
const diffReviewCloseIcon = registerIcon('diff-review-close', Codicon.close, nls.localize('diffReviewCloseIcon', 'Icon for \'Close\' in diff review.'));
export class DiffReview extends Disposable {
public static _ttPolicy = createTrustedTypesPolicy('diffReview', { createHTML: value => value });
private readonly _diffEditor: DiffEditorWidget;
private _isVisible: boolean;
public readonly shadow: FastDomNode<HTMLElement>;
private readonly _actionBar: ActionBar;
public readonly actionBarContainer: FastDomNode<HTMLElement>;
public readonly domNode: FastDomNode<HTMLElement>;
private readonly _content: FastDomNode<HTMLElement>;
private readonly scrollbar: DomScrollableElement;
private _diffs: Diff[];
private _currentDiff: Diff | null;
constructor(
diffEditor: DiffEditorWidget,
@ILanguageService private readonly _languageService: ILanguageService,
@IAudioCueService private readonly _audioCueService: IAudioCueService,
@IConfigurationService private readonly _configurationService: IConfigurationService
) {
super();
this._diffEditor = diffEditor;
this._isVisible = false;
this.shadow = createFastDomNode(document.createElement('div'));
this.shadow.setClassName('diff-review-shadow');
this.actionBarContainer = createFastDomNode(document.createElement('div'));
this.actionBarContainer.setClassName('diff-review-actions');
this._actionBar = this._register(new ActionBar(
this.actionBarContainer.domNode
));
this._actionBar.push(new Action('diffreview.close', nls.localize('label.close', "Close"), 'close-diff-review ' + ThemeIcon.asClassName(diffReviewCloseIcon), true, async () => this.hide()), { label: false, icon: true });
this.domNode = createFastDomNode(document.createElement('div'));
this.domNode.setClassName('diff-review monaco-editor-background');
this._content = createFastDomNode(document.createElement('div'));
this._content.setClassName('diff-review-content');
this._content.setAttribute('role', 'code');
this.scrollbar = this._register(new DomScrollableElement(this._content.domNode, {}));
this.domNode.domNode.appendChild(this.scrollbar.getDomNode());
this._register(diffEditor.onDidUpdateDiff(() => {
if (!this._isVisible) {
return;
}
this._diffs = this._compute();
this._render();
}));
this._register(diffEditor.getModifiedEditor().onDidChangeCursorPosition(() => {
if (!this._isVisible) {
return;
}
this._render();
}));
this._register(dom.addStandardDisposableListener(this.domNode.domNode, 'click', (e) => {
e.preventDefault();
const row = dom.findParentWithClass(e.target, 'diff-review-row');
if (row) {
this._goToRow(row);
}
}));
this._register(dom.addStandardDisposableListener(this.domNode.domNode, 'keydown', (e) => {
if (
e.equals(KeyCode.DownArrow)
|| e.equals(KeyMod.CtrlCmd | KeyCode.DownArrow)
|| e.equals(KeyMod.Alt | KeyCode.DownArrow)
) {
e.preventDefault();
this._goToRow(this._getNextRow(), 'next');
}
if (
e.equals(KeyCode.UpArrow)
|| e.equals(KeyMod.CtrlCmd | KeyCode.UpArrow)
|| e.equals(KeyMod.Alt | KeyCode.UpArrow)
) {
e.preventDefault();
this._goToRow(this._getPrevRow(), 'previous');
}
if (
e.equals(KeyCode.Escape)
|| e.equals(KeyMod.CtrlCmd | KeyCode.Escape)
|| e.equals(KeyMod.Alt | KeyCode.Escape)
|| e.equals(KeyMod.Shift | KeyCode.Escape)
|| e.equals(KeyCode.Space)
|| e.equals(KeyCode.Enter)
) {
e.preventDefault();
this.accept();
}
}));
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('accessibility.verbosity.diffEditor')) {
this._diffEditor.updateOptions({ accessibilityVerbose: this._configurationService.getValue('accessibility.verbosity.diffEditor') });
}
}));
this._diffs = [];
this._currentDiff = null;
}
public prev(): void {
let index = 0;
if (!this._isVisible) {
this._diffs = this._compute();
}
if (this._isVisible) {
let currentIndex = -1;
for (let i = 0, len = this._diffs.length; i < len; i++) {
if (this._diffs[i] === this._currentDiff) {
currentIndex = i;
break;
}
}
index = (this._diffs.length + currentIndex - 1);
} else {
index = this._findDiffIndex(this._diffEditor.getPosition()!);
}
if (this._diffs.length === 0) {
// Nothing to do
return;
}
index = index % this._diffs.length;
const entries = this._diffs[index].entries;
this._diffEditor.setPosition(new Position(entries[0].modifiedLineStart, 1));
this._diffEditor.setSelection({ startColumn: 1, startLineNumber: entries[0].modifiedLineStart, endColumn: Constants.MAX_SAFE_SMALL_INTEGER, endLineNumber: entries[entries.length - 1].modifiedLineEnd });
this._isVisible = true;
this._diffEditor.doLayout();
this._render();
this._goToRow(this._getPrevRow(), 'previous');
}
public next(): void {
let index = 0;
if (!this._isVisible) {
this._diffs = this._compute();
}
if (this._isVisible) {
let currentIndex = -1;
for (let i = 0, len = this._diffs.length; i < len; i++) {
if (this._diffs[i] === this._currentDiff) {
currentIndex = i;
break;
}
}
index = (currentIndex + 1);
} else {
index = this._findDiffIndex(this._diffEditor.getPosition()!);
}
if (this._diffs.length === 0) {
// Nothing to do
return;
}
index = index % this._diffs.length;
const entries = this._diffs[index].entries;
this._diffEditor.setPosition(new Position(entries[0].modifiedLineStart, 1));
this._diffEditor.setSelection({ startColumn: 1, startLineNumber: entries[0].modifiedLineStart, endColumn: Constants.MAX_SAFE_SMALL_INTEGER, endLineNumber: entries[entries.length - 1].modifiedLineEnd });
this._isVisible = true;
this._diffEditor.doLayout();
this._render();
this._goToRow(this._getNextRow(), 'next');
}
private accept(): void {
let jumpToLineNumber = -1;
const current = this._getCurrentFocusedRow();
if (current) {
const lineNumber = parseInt(current.getAttribute('data-line')!, 10);
if (!isNaN(lineNumber)) {
jumpToLineNumber = lineNumber;
}
}
this.hide();
if (jumpToLineNumber !== -1) {
this._diffEditor.setPosition(new Position(jumpToLineNumber, 1));
this._diffEditor.revealPosition(new Position(jumpToLineNumber, 1), ScrollType.Immediate);
}
}
private hide(): void {
this._isVisible = false;
this._diffEditor.updateOptions({ readOnly: false });
this._diffEditor.focus();
this._diffEditor.doLayout();
this._render();
}
private _getPrevRow(): HTMLElement {
const current = this._getCurrentFocusedRow();
if (!current) {
return this._getFirstRow();
}
if (current.previousElementSibling) {
return <HTMLElement>current.previousElementSibling;
}
return current;
}
private _getNextRow(): HTMLElement {
const current = this._getCurrentFocusedRow();
if (!current) {
return this._getFirstRow();
}
if (current.nextElementSibling) {
return <HTMLElement>current.nextElementSibling;
}
return current;
}
private _getFirstRow(): HTMLElement {
return <HTMLElement>this.domNode.domNode.querySelector('.diff-review-row');
}
private _getCurrentFocusedRow(): HTMLElement | null {
const result = <HTMLElement>document.activeElement;
if (result && /diff-review-row/.test(result.className)) {
return result;
}
return null;
}
private _goToRow(row: HTMLElement, type?: 'next' | 'previous'): void {
const current = this._getCurrentFocusedRow();
row.tabIndex = 0;
row.focus();
if (current && current !== row) {
current.tabIndex = -1;
}
const element = !type ? current : type === 'next' ? current?.nextElementSibling : current?.previousElementSibling;
if (element?.classList.contains(DiffEditorLineClasses.Insert)) {
this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { allowManyInParallel: true });
} else if (element?.classList.contains(DiffEditorLineClasses.Delete)) {
this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { allowManyInParallel: true });
}
this.scrollbar.scanDomNode();
}
public isVisible(): boolean {
return this._isVisible;
}
private _width: number = 0;
public layout(top: number, width: number, height: number): void {
this._width = width;
this.shadow.setTop(top - 6);
this.shadow.setWidth(width);
this.shadow.setHeight(this._isVisible ? 6 : 0);
this.domNode.setTop(top);
this.domNode.setWidth(width);
this.domNode.setHeight(height);
this._content.setHeight(height);
this._content.setWidth(width);
if (this._isVisible) {
this.actionBarContainer.setAttribute('aria-hidden', 'false');
this.actionBarContainer.setDisplay('block');
} else {
this.actionBarContainer.setAttribute('aria-hidden', 'true');
this.actionBarContainer.setDisplay('none');
}
}
private _compute(): Diff[] {
const lineChanges = this._diffEditor.getLineChanges();
if (!lineChanges || lineChanges.length === 0) {
return [];
}
const originalModel = this._diffEditor.getOriginalEditor().getModel();
const modifiedModel = this._diffEditor.getModifiedEditor().getModel();
if (!originalModel || !modifiedModel) {
return [];
}
return DiffReview._mergeAdjacent(lineChanges, originalModel.getLineCount(), modifiedModel.getLineCount());
}
private static _mergeAdjacent(lineChanges: ILineChange[], originalLineCount: number, modifiedLineCount: number): Diff[] {
if (!lineChanges || lineChanges.length === 0) {
return [];
}
const diffs: Diff[] = [];
let diffsLength = 0;
for (let i = 0, len = lineChanges.length; i < len; i++) {
const lineChange = lineChanges[i];
const originalStart = lineChange.originalStartLineNumber;
const originalEnd = lineChange.originalEndLineNumber;
const modifiedStart = lineChange.modifiedStartLineNumber;
const modifiedEnd = lineChange.modifiedEndLineNumber;
const r: DiffEntry[] = [];
let rLength = 0;
// Emit before anchors
{
const originalEqualAbove = (originalEnd === 0 ? originalStart : originalStart - 1);
const modifiedEqualAbove = (modifiedEnd === 0 ? modifiedStart : modifiedStart - 1);
// Make sure we don't step into the previous diff
let minOriginal = 1;
let minModified = 1;
if (i > 0) {
const prevLineChange = lineChanges[i - 1];
if (prevLineChange.originalEndLineNumber === 0) {
minOriginal = prevLineChange.originalStartLineNumber + 1;
} else {
minOriginal = prevLineChange.originalEndLineNumber + 1;
}
if (prevLineChange.modifiedEndLineNumber === 0) {
minModified = prevLineChange.modifiedStartLineNumber + 1;
} else {
minModified = prevLineChange.modifiedEndLineNumber + 1;
}
}
let fromOriginal = originalEqualAbove - DIFF_LINES_PADDING + 1;
let fromModified = modifiedEqualAbove - DIFF_LINES_PADDING + 1;
if (fromOriginal < minOriginal) {
const delta = minOriginal - fromOriginal;
fromOriginal = fromOriginal + delta;
fromModified = fromModified + delta;
}
if (fromModified < minModified) {
const delta = minModified - fromModified;
fromOriginal = fromOriginal + delta;
fromModified = fromModified + delta;
}
r[rLength++] = new DiffEntry(
fromOriginal, originalEqualAbove,
fromModified, modifiedEqualAbove
);
}
// Emit deleted lines
{
if (originalEnd !== 0) {
r[rLength++] = new DiffEntry(originalStart, originalEnd, 0, 0);
}
}
// Emit inserted lines
{
if (modifiedEnd !== 0) {
r[rLength++] = new DiffEntry(0, 0, modifiedStart, modifiedEnd);
}
}
// Emit after anchors
{
const originalEqualBelow = (originalEnd === 0 ? originalStart + 1 : originalEnd + 1);
const modifiedEqualBelow = (modifiedEnd === 0 ? modifiedStart + 1 : modifiedEnd + 1);
// Make sure we don't step into the next diff
let maxOriginal = originalLineCount;
let maxModified = modifiedLineCount;
if (i + 1 < len) {
const nextLineChange = lineChanges[i + 1];
if (nextLineChange.originalEndLineNumber === 0) {
maxOriginal = nextLineChange.originalStartLineNumber;
} else {
maxOriginal = nextLineChange.originalStartLineNumber - 1;
}
if (nextLineChange.modifiedEndLineNumber === 0) {
maxModified = nextLineChange.modifiedStartLineNumber;
} else {
maxModified = nextLineChange.modifiedStartLineNumber - 1;
}
}
let toOriginal = originalEqualBelow + DIFF_LINES_PADDING - 1;
let toModified = modifiedEqualBelow + DIFF_LINES_PADDING - 1;
if (toOriginal > maxOriginal) {
const delta = maxOriginal - toOriginal;
toOriginal = toOriginal + delta;
toModified = toModified + delta;
}
if (toModified > maxModified) {
const delta = maxModified - toModified;
toOriginal = toOriginal + delta;
toModified = toModified + delta;
}
r[rLength++] = new DiffEntry(
originalEqualBelow, toOriginal,
modifiedEqualBelow, toModified,
);
}
diffs[diffsLength++] = new Diff(r);
}
// Merge adjacent diffs
let curr: DiffEntry[] = diffs[0].entries;
const r: Diff[] = [];
let rLength = 0;
for (let i = 1, len = diffs.length; i < len; i++) {
const thisDiff = diffs[i].entries;
const currLast = curr[curr.length - 1];
const thisFirst = thisDiff[0];
if (
currLast.getType() === DiffEntryType.Equal
&& thisFirst.getType() === DiffEntryType.Equal
&& thisFirst.originalLineStart <= currLast.originalLineEnd
) {
// We are dealing with equal lines that overlap
curr[curr.length - 1] = new DiffEntry(
currLast.originalLineStart, thisFirst.originalLineEnd,
currLast.modifiedLineStart, thisFirst.modifiedLineEnd
);
curr = curr.concat(thisDiff.slice(1));
continue;
}
r[rLength++] = new Diff(curr);
curr = thisDiff;
}
r[rLength++] = new Diff(curr);
return r;
}
private _findDiffIndex(pos: Position): number {
const lineNumber = pos.lineNumber;
for (let i = 0, len = this._diffs.length; i < len; i++) {
const diff = this._diffs[i].entries;
const lastModifiedLine = diff[diff.length - 1].modifiedLineEnd;
if (lineNumber <= lastModifiedLine) {
return i;
}
}
return 0;
}
private _render(): void {
const originalOptions = this._diffEditor.getOriginalEditor().getOptions();
const modifiedOptions = this._diffEditor.getModifiedEditor().getOptions();
const originalModel = this._diffEditor.getOriginalEditor().getModel();
const modifiedModel = this._diffEditor.getModifiedEditor().getModel();
const originalModelOpts = originalModel!.getOptions();
const modifiedModelOpts = modifiedModel!.getOptions();
if (!this._isVisible || !originalModel || !modifiedModel) {
dom.clearNode(this._content.domNode);
this._currentDiff = null;
this.scrollbar.scanDomNode();
return;
}
this._diffEditor.updateOptions({ readOnly: true });
const diffIndex = this._findDiffIndex(this._diffEditor.getPosition()!);
if (this._diffs[diffIndex] === this._currentDiff) {
return;
}
this._currentDiff = this._diffs[diffIndex];
const diffs = this._diffs[diffIndex].entries;
const container = document.createElement('div');
container.className = 'diff-review-table';
container.setAttribute('role', 'list');
container.setAttribute('aria-label', 'Difference review. Use "Stage | Unstage | Revert Selected Ranges" commands');
applyFontInfo(container, modifiedOptions.get(EditorOption.fontInfo));
let minOriginalLine = 0;
let maxOriginalLine = 0;
let minModifiedLine = 0;
let maxModifiedLine = 0;
for (let i = 0, len = diffs.length; i < len; i++) {
const diffEntry = diffs[i];
const originalLineStart = diffEntry.originalLineStart;
const originalLineEnd = diffEntry.originalLineEnd;
const modifiedLineStart = diffEntry.modifiedLineStart;
const modifiedLineEnd = diffEntry.modifiedLineEnd;
if (originalLineStart !== 0 && ((minOriginalLine === 0 || originalLineStart < minOriginalLine))) {
minOriginalLine = originalLineStart;
}
if (originalLineEnd !== 0 && ((maxOriginalLine === 0 || originalLineEnd > maxOriginalLine))) {
maxOriginalLine = originalLineEnd;
}
if (modifiedLineStart !== 0 && ((minModifiedLine === 0 || modifiedLineStart < minModifiedLine))) {
minModifiedLine = modifiedLineStart;
}
if (modifiedLineEnd !== 0 && ((maxModifiedLine === 0 || modifiedLineEnd > maxModifiedLine))) {
maxModifiedLine = modifiedLineEnd;
}
}
const header = document.createElement('div');
header.className = 'diff-review-row';
const cell = document.createElement('div');
cell.className = 'diff-review-cell diff-review-summary';
const originalChangedLinesCnt = maxOriginalLine - minOriginalLine + 1;
const modifiedChangedLinesCnt = maxModifiedLine - minModifiedLine + 1;
cell.appendChild(document.createTextNode(`${diffIndex + 1}/${this._diffs.length}: @@ -${minOriginalLine},${originalChangedLinesCnt} +${minModifiedLine},${modifiedChangedLinesCnt} @@`));
header.setAttribute('data-line', String(minModifiedLine));
const getAriaLines = (lines: number) => {
if (lines === 0) {
return nls.localize('no_lines_changed', "no lines changed");
} else if (lines === 1) {
return nls.localize('one_line_changed', "1 line changed");
} else {
return nls.localize('more_lines_changed', "{0} lines changed", lines);
}
};
const originalChangedLinesCntAria = getAriaLines(originalChangedLinesCnt);
const modifiedChangedLinesCntAria = getAriaLines(modifiedChangedLinesCnt);
header.setAttribute('aria-label', nls.localize({
key: 'header',
comment: [
'This is the ARIA label for a git diff header.',
'A git diff header looks like this: @@ -154,12 +159,39 @@.',
'That encodes that at original line 154 (which is now line 159), 12 lines were removed/changed with 39 lines.',
'Variables 0 and 1 refer to the diff index out of total number of diffs.',
'Variables 2 and 4 will be numbers (a line number).',
'Variables 3 and 5 will be "no lines changed", "1 line changed" or "X lines changed", localized separately.'
]
}, "Difference {0} of {1}: original line {2}, {3}, modified line {4}, {5}", (diffIndex + 1), this._diffs.length, minOriginalLine, originalChangedLinesCntAria, minModifiedLine, modifiedChangedLinesCntAria));
header.appendChild(cell);
// @@ -504,7 +517,7 @@
header.setAttribute('role', 'listitem');
container.appendChild(header);
const lineHeight = modifiedOptions.get(EditorOption.lineHeight);
let modLine = minModifiedLine;
for (let i = 0, len = diffs.length; i < len; i++) {
const diffEntry = diffs[i];
DiffReview._renderSection(container, diffEntry, modLine, lineHeight, this._width, originalOptions, originalModel, originalModelOpts, modifiedOptions, modifiedModel, modifiedModelOpts, this._languageService.languageIdCodec);
if (diffEntry.modifiedLineStart !== 0) {
modLine = diffEntry.modifiedLineEnd;
}
}
dom.clearNode(this._content.domNode);
this._content.domNode.appendChild(container);
this.scrollbar.scanDomNode();
}
private static _renderSection(
dest: HTMLElement, diffEntry: DiffEntry, modLine: number, lineHeight: number, width: number,
originalOptions: IComputedEditorOptions, originalModel: ITextModel, originalModelOpts: TextModelResolvedOptions,
modifiedOptions: IComputedEditorOptions, modifiedModel: ITextModel, modifiedModelOpts: TextModelResolvedOptions,
languageIdCodec: ILanguageIdCodec
): void {
const type = diffEntry.getType();
let rowClassName: string = 'diff-review-row';
let lineNumbersExtraClassName: string = '';
const spacerClassName: string = 'diff-review-spacer';
let spacerIcon: ThemeIcon | null = null;
switch (type) {
case DiffEntryType.Insert:
rowClassName = 'diff-review-row line-insert';
lineNumbersExtraClassName = ' char-insert';
spacerIcon = diffReviewInsertIcon;
break;
case DiffEntryType.Delete:
rowClassName = 'diff-review-row line-delete';
lineNumbersExtraClassName = ' char-delete';
spacerIcon = diffReviewRemoveIcon;
break;
}
const originalLineStart = diffEntry.originalLineStart;
const originalLineEnd = diffEntry.originalLineEnd;
const modifiedLineStart = diffEntry.modifiedLineStart;
const modifiedLineEnd = diffEntry.modifiedLineEnd;
const cnt = Math.max(
modifiedLineEnd - modifiedLineStart,
originalLineEnd - originalLineStart
);
const originalLayoutInfo = originalOptions.get(EditorOption.layoutInfo);
const originalLineNumbersWidth = originalLayoutInfo.glyphMarginWidth + originalLayoutInfo.lineNumbersWidth;
const modifiedLayoutInfo = modifiedOptions.get(EditorOption.layoutInfo);
const modifiedLineNumbersWidth = 10 + modifiedLayoutInfo.glyphMarginWidth + modifiedLayoutInfo.lineNumbersWidth;
for (let i = 0; i <= cnt; i++) {
const originalLine = (originalLineStart === 0 ? 0 : originalLineStart + i);
const modifiedLine = (modifiedLineStart === 0 ? 0 : modifiedLineStart + i);
const row = document.createElement('div');
row.style.minWidth = width + 'px';
row.className = rowClassName;
row.setAttribute('role', 'listitem');
if (modifiedLine !== 0) {
modLine = modifiedLine;
}
row.setAttribute('data-line', String(modLine));
const cell = document.createElement('div');
cell.className = 'diff-review-cell';
cell.style.height = `${lineHeight}px`;
row.appendChild(cell);
const originalLineNumber = document.createElement('span');
originalLineNumber.style.width = (originalLineNumbersWidth + 'px');
originalLineNumber.style.minWidth = (originalLineNumbersWidth + 'px');
originalLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName;
if (originalLine !== 0) {
originalLineNumber.appendChild(document.createTextNode(String(originalLine)));
} else {
originalLineNumber.innerText = '\u00a0';
}
cell.appendChild(originalLineNumber);
const modifiedLineNumber = document.createElement('span');
modifiedLineNumber.style.width = (modifiedLineNumbersWidth + 'px');
modifiedLineNumber.style.minWidth = (modifiedLineNumbersWidth + 'px');
modifiedLineNumber.style.paddingRight = '10px';
modifiedLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName;
if (modifiedLine !== 0) {
modifiedLineNumber.appendChild(document.createTextNode(String(modifiedLine)));
} else {
modifiedLineNumber.innerText = '\u00a0';
}
cell.appendChild(modifiedLineNumber);
const spacer = document.createElement('span');
spacer.className = spacerClassName;
if (spacerIcon) {
const spacerCodicon = document.createElement('span');
spacerCodicon.className = ThemeIcon.asClassName(spacerIcon);
spacerCodicon.innerText = '\u00a0\u00a0';
spacer.appendChild(spacerCodicon);
} else {
spacer.innerText = '\u00a0\u00a0';
}
cell.appendChild(spacer);
let lineContent: string;
if (modifiedLine !== 0) {
let html: string | TrustedHTML = this._renderLine(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, modifiedLine, languageIdCodec);
if (DiffReview._ttPolicy) {
html = DiffReview._ttPolicy.createHTML(html as string);
}
cell.insertAdjacentHTML('beforeend', html as string);
lineContent = modifiedModel.getLineContent(modifiedLine);
} else {
let html: string | TrustedHTML = this._renderLine(originalModel, originalOptions, originalModelOpts.tabSize, originalLine, languageIdCodec);
if (DiffReview._ttPolicy) {
html = DiffReview._ttPolicy.createHTML(html as string);
}
cell.insertAdjacentHTML('beforeend', html as string);
lineContent = originalModel.getLineContent(originalLine);
}
if (lineContent.length === 0) {
lineContent = nls.localize('blankLine', "blank");
}
let ariaLabel: string = '';
switch (type) {
case DiffEntryType.Equal:
if (originalLine === modifiedLine) {
ariaLabel = nls.localize({ key: 'unchangedLine', comment: ['The placeholders are contents of the line and should not be translated.'] }, "{0} unchanged line {1}", lineContent, originalLine);
} else {
ariaLabel = nls.localize('equalLine', "{0} original line {1} modified line {2}", lineContent, originalLine, modifiedLine);
}
break;
case DiffEntryType.Insert:
ariaLabel = nls.localize('insertLine', "+ {0} modified line {1}", lineContent, modifiedLine);
break;
case DiffEntryType.Delete:
ariaLabel = nls.localize('deleteLine', "- {0} original line {1}", lineContent, originalLine);
break;
}
row.setAttribute('aria-label', ariaLabel);
dest.appendChild(row);
}
}
private static _renderLine(model: ITextModel, options: IComputedEditorOptions, tabSize: number, lineNumber: number, languageIdCodec: ILanguageIdCodec): string {
const lineContent = model.getLineContent(lineNumber);
const fontInfo = options.get(EditorOption.fontInfo);
const lineTokens = LineTokens.createEmpty(lineContent, languageIdCodec);
const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, model.mightContainNonBasicASCII());
const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, model.mightContainRTL());
const r = renderViewLine(new RenderLineInput(
(fontInfo.isMonospace && !options.get(EditorOption.disableMonospaceOptimizations)),
fontInfo.canUseHalfwidthRightwardsArrow,
lineContent,
false,
isBasicASCII,
containsRTL,
0,
lineTokens,
[],
tabSize,
0,
fontInfo.spaceWidth,
fontInfo.middotWidth,
fontInfo.wsmiddotWidth,
options.get(EditorOption.stopRenderingLineAfter),
options.get(EditorOption.renderWhitespace),
options.get(EditorOption.renderControlCharacters),
options.get(EditorOption.fontLigatures) !== EditorFontLigatures.OFF,
null
));
return r.html;
}
}
// theming
@@ -7,21 +7,18 @@ import * as objects from 'vs/base/common/objects';
import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffEditorWidget, IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditorWidget';
import { DiffEditorWidget2, IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2';
import { ConfigurationChangedEvent, IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IEditorProgressService } from 'vs/platform/progress/common/progress';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2';
import { IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
export class EmbeddedCodeEditorWidget extends CodeEditorWidget {
@@ -69,54 +66,6 @@ export class EmbeddedCodeEditorWidget extends CodeEditorWidget {
}
}
/**
* @deprecated Use EmbeddedDiffEditorWidget2 instead.
*/
export class EmbeddedDiffEditorWidget extends DiffEditorWidget {
private readonly _parentEditor: ICodeEditor;
private readonly _overwriteOptions: IDiffEditorOptions;
constructor(
domElement: HTMLElement,
options: Readonly<IDiffEditorConstructionOptions>,
codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions,
parentEditor: ICodeEditor,
@IContextKeyService contextKeyService: IContextKeyService,
@IInstantiationService instantiationService: IInstantiationService,
@ICodeEditorService codeEditorService: ICodeEditorService,
@IThemeService themeService: IThemeService,
@INotificationService notificationService: INotificationService,
@IContextMenuService contextMenuService: IContextMenuService,
@IClipboardService clipboardService: IClipboardService,
@IEditorProgressService editorProgressService: IEditorProgressService,
) {
super(domElement, parentEditor.getRawOptions(), codeEditorWidgetOptions, clipboardService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService);
this._parentEditor = parentEditor;
this._overwriteOptions = options;
// Overwrite parent's options
super.updateOptions(this._overwriteOptions);
this._register(parentEditor.onDidChangeConfiguration(e => this._onParentConfigurationChanged(e)));
}
getParentEditor(): ICodeEditor {
return this._parentEditor;
}
private _onParentConfigurationChanged(e: ConfigurationChangedEvent): void {
super.updateOptions(this._parentEditor.getRawOptions());
super.updateOptions(this._overwriteOptions);
}
override updateOptions(newOptions: IEditorOptions): void {
objects.mixin(this._overwriteOptions, newOptions, true);
super.updateOptions(this._overwriteOptions);
}
}
/**
* TODO: Rename to EmbeddedDiffEditorWidget once EmbeddedDiffEditorWidget is removed.
*/
@@ -9,7 +9,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { StopWatch } from 'vs/base/common/stopwatch';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { IDocumentDiff, IDocumentDiffProvider, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { ITextModel } from 'vs/editor/common/model';
import { DiffAlgorithmName, IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -53,7 +53,7 @@ export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider, I
return {
changes: [
new LineRangeMapping(
new DetailedLineRangeMapping(
new LineRange(1, 2),
new LineRange(1, modified.getLineCount() + 1),
[
@@ -217,36 +217,30 @@ const editorConfiguration: IConfigurationNode = {
'diffEditor.hideUnchangedRegions.enabled': {
type: 'boolean',
default: diffEditorDefaultOptions.hideUnchangedRegions.enabled,
markdownDescription: nls.localize('hideUnchangedRegions.enabled', "Controls whether the diff editor shows unchanged regions. Only works when {0} is set.", '`#diffEditor.experimental.useVersion2#`'),
markdownDescription: nls.localize('hideUnchangedRegions.enabled', "Controls whether the diff editor shows unchanged regions."),
},
'diffEditor.hideUnchangedRegions.revealLineCount': {
type: 'integer',
default: diffEditorDefaultOptions.hideUnchangedRegions.revealLineCount,
markdownDescription: nls.localize('hideUnchangedRegions.revealLineCount', "Controls how many lines are used for unchanged regions. Only works when {0} is set.", '`#diffEditor.experimental.useVersion2#`'),
markdownDescription: nls.localize('hideUnchangedRegions.revealLineCount', "Controls how many lines are used for unchanged regions."),
minimum: 1,
},
'diffEditor.hideUnchangedRegions.minimumLineCount': {
type: 'integer',
default: diffEditorDefaultOptions.hideUnchangedRegions.minimumLineCount,
markdownDescription: nls.localize('hideUnchangedRegions.minimumLineCount', "Controls how many lines are used as a minimum for unchanged regions. Only works when {0} is set.", '`#diffEditor.experimental.useVersion2#`'),
markdownDescription: nls.localize('hideUnchangedRegions.minimumLineCount', "Controls how many lines are used as a minimum for unchanged regions."),
minimum: 1,
},
'diffEditor.hideUnchangedRegions.contextLineCount': {
type: 'integer',
default: diffEditorDefaultOptions.hideUnchangedRegions.contextLineCount,
markdownDescription: nls.localize('hideUnchangedRegions.contextLineCount', "Controls how many lines are used as context when comparing unchanged regions. Only works when {0} is set.", '`#diffEditor.experimental.useVersion2#`'),
markdownDescription: nls.localize('hideUnchangedRegions.contextLineCount', "Controls how many lines are used as context when comparing unchanged regions."),
minimum: 1,
},
'diffEditor.experimental.showMoves': {
type: 'boolean',
default: diffEditorDefaultOptions.experimental.showMoves,
markdownDescription: nls.localize('showMoves', "Controls whether the diff editor should show detected code moves. Only works when {0} is set.", '`#diffEditor.experimental.useVersion2#`')
},
'diffEditor.experimental.useVersion2': {
type: 'boolean',
default: true,
description: nls.localize('useVersion2', "Controls whether the diff editor uses the new or the old implementation."),
tags: ['experimental'],
markdownDescription: nls.localize('showMoves', "Controls whether the diff editor should show detected code moves.")
},
'diffEditor.experimental.showEmptyDecorations': {
type: 'boolean',
+1 -2
View File
@@ -13,7 +13,6 @@ import { Constants } from 'vs/base/common/uint';
import { FontInfo } from 'vs/editor/common/config/fontInfo';
import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults';
import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/core/wordHelper';
import { IDocumentDiffProvider } from 'vs/editor/common/diff/documentDiffProvider';
import * as nls from 'vs/nls';
import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
@@ -817,7 +816,7 @@ export interface IDiffEditorBaseOptions {
/**
* Diff Algorithm
*/
diffAlgorithm?: 'legacy' | 'advanced' | IDocumentDiffProvider;
diffAlgorithm?: 'legacy' | 'advanced';
/**
* Whether the diff editor aria label should be verbose.
+169 -58
View File
@@ -6,6 +6,7 @@
import { BugIndicatingError } from 'vs/base/common/errors';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { Range } from 'vs/editor/common/core/range';
import { findFirstIdxMonotonousOrArrLen, findLastIdxMonotonous, findLastMonotonous } from 'vs/base/common/arraysFind';
/**
* A range of lines (1-based).
@@ -40,66 +41,11 @@ export class LineRange {
if (lineRanges.length === 0) {
return [];
}
let result = lineRanges[0];
let result = new LineRangeSet(lineRanges[0].slice());
for (let i = 1; i < lineRanges.length; i++) {
result = this.join(result, lineRanges[i]);
result = result.getUnion(new LineRangeSet(lineRanges[i].slice()));
}
return result;
}
/**
* @param lineRanges1 Must be sorted.
* @param lineRanges2 Must be sorted.
*/
public static join(lineRanges1: readonly LineRange[], lineRanges2: readonly LineRange[]): readonly LineRange[] {
if (lineRanges1.length === 0) {
return lineRanges2;
}
if (lineRanges2.length === 0) {
return lineRanges1;
}
const result: LineRange[] = [];
let i1 = 0;
let i2 = 0;
let current: LineRange | null = null;
while (i1 < lineRanges1.length || i2 < lineRanges2.length) {
let next: LineRange | null = null;
if (i1 < lineRanges1.length && i2 < lineRanges2.length) {
const lineRange1 = lineRanges1[i1];
const lineRange2 = lineRanges2[i2];
if (lineRange1.startLineNumber < lineRange2.startLineNumber) {
next = lineRange1;
i1++;
} else {
next = lineRange2;
i2++;
}
} else if (i1 < lineRanges1.length) {
next = lineRanges1[i1];
i1++;
} else {
next = lineRanges2[i2];
i2++;
}
if (current === null) {
current = next;
} else {
if (current.endLineNumberExclusive >= next.startLineNumber) {
// merge
current = new LineRange(current.startLineNumber, Math.max(current.endLineNumberExclusive, next.endLineNumberExclusive));
} else {
// push
result.push(current);
current = next;
}
}
}
if (current !== null) {
result.push(current);
}
return result;
return result.ranges;
}
public static ofLength(startLineNumber: number, length: number): LineRange {
@@ -251,3 +197,168 @@ export class LineRange {
}
export type ISerializedLineRange = [startLineNumber: number, endLineNumberExclusive: number];
export class LineRangeSet {
constructor(
/**
* Sorted by start line number.
* No two line ranges are touching or intersecting.
*/
private readonly _normalizedRanges: LineRange[] = []
) {
}
get ranges(): readonly LineRange[] {
return this._normalizedRanges;
}
addRange(range: LineRange): void {
if (range.length === 0) {
return;
}
// Idea: Find joinRange such that:
// replaceRange = _normalizedRanges.replaceRange(joinRange, range.joinAll(joinRange.map(idx => this._normalizedRanges[idx])))
// idx of first element that touches range or that is after range
const joinRangeStartIdx = findFirstIdxMonotonousOrArrLen(this._normalizedRanges, r => r.endLineNumberExclusive >= range.startLineNumber);
// idx of element after { last element that touches range or that is before range }
const joinRangeEndIdxExclusive = findLastIdxMonotonous(this._normalizedRanges, r => r.startLineNumber <= range.endLineNumberExclusive) + 1;
if (joinRangeStartIdx === joinRangeEndIdxExclusive) {
// If there is no element that touches range, then joinRangeStartIdx === joinRangeEndIdxExclusive and that value is the index of the element after range
this._normalizedRanges.splice(joinRangeStartIdx, 0, range);
} else if (joinRangeStartIdx === joinRangeEndIdxExclusive - 1) {
// Else, there is an element that touches range and in this case it is both the first and last element. Thus we can replace it
const joinRange = this._normalizedRanges[joinRangeStartIdx];
this._normalizedRanges[joinRangeStartIdx] = joinRange.join(range);
} else {
// First and last element are different - we need to replace the entire range
const joinRange = this._normalizedRanges[joinRangeStartIdx].join(this._normalizedRanges[joinRangeEndIdxExclusive - 1]).join(range);
this._normalizedRanges.splice(joinRangeStartIdx, joinRangeEndIdxExclusive - joinRangeStartIdx, joinRange);
}
}
contains(lineNumber: number): boolean {
const rangeThatStartsBeforeEnd = findLastMonotonous(this._normalizedRanges, r => r.startLineNumber <= lineNumber);
return !!rangeThatStartsBeforeEnd && rangeThatStartsBeforeEnd.endLineNumberExclusive > lineNumber;
}
intersects(range: LineRange): boolean {
const rangeThatStartsBeforeEnd = findLastMonotonous(this._normalizedRanges, r => r.startLineNumber < range.endLineNumberExclusive);
return !!rangeThatStartsBeforeEnd && rangeThatStartsBeforeEnd.endLineNumberExclusive > range.startLineNumber;
}
getUnion(other: LineRangeSet): LineRangeSet {
if (this._normalizedRanges.length === 0) {
return other;
}
if (other._normalizedRanges.length === 0) {
return this;
}
const result: LineRange[] = [];
let i1 = 0;
let i2 = 0;
let current: LineRange | null = null;
while (i1 < this._normalizedRanges.length || i2 < other._normalizedRanges.length) {
let next: LineRange | null = null;
if (i1 < this._normalizedRanges.length && i2 < other._normalizedRanges.length) {
const lineRange1 = this._normalizedRanges[i1];
const lineRange2 = other._normalizedRanges[i2];
if (lineRange1.startLineNumber < lineRange2.startLineNumber) {
next = lineRange1;
i1++;
} else {
next = lineRange2;
i2++;
}
} else if (i1 < this._normalizedRanges.length) {
next = this._normalizedRanges[i1];
i1++;
} else {
next = other._normalizedRanges[i2];
i2++;
}
if (current === null) {
current = next;
} else {
if (current.endLineNumberExclusive >= next.startLineNumber) {
// merge
current = new LineRange(current.startLineNumber, Math.max(current.endLineNumberExclusive, next.endLineNumberExclusive));
} else {
// push
result.push(current);
current = next;
}
}
}
if (current !== null) {
result.push(current);
}
return new LineRangeSet(result);
}
/**
* Subtracts all ranges in this set from `range` and returns the result.
*/
subtractFrom(range: LineRange): LineRangeSet {
// idx of first element that touches range or that is after range
const joinRangeStartIdx = findFirstIdxMonotonousOrArrLen(this._normalizedRanges, r => r.endLineNumberExclusive >= range.startLineNumber);
// idx of element after { last element that touches range or that is before range }
const joinRangeEndIdxExclusive = findLastIdxMonotonous(this._normalizedRanges, r => r.startLineNumber <= range.endLineNumberExclusive) + 1;
if (joinRangeStartIdx === joinRangeEndIdxExclusive) {
return new LineRangeSet([range]);
}
const result: LineRange[] = [];
let startLineNumber = range.startLineNumber;
for (let i = joinRangeStartIdx; i < joinRangeEndIdxExclusive; i++) {
const r = this._normalizedRanges[i];
if (r.startLineNumber > startLineNumber) {
result.push(new LineRange(startLineNumber, r.startLineNumber));
}
startLineNumber = r.endLineNumberExclusive;
}
if (startLineNumber < range.endLineNumberExclusive) {
result.push(new LineRange(startLineNumber, range.endLineNumberExclusive));
}
return new LineRangeSet(result);
}
toString() {
return this._normalizedRanges.map(r => r.toString()).join(', ');
}
getIntersection(other: LineRangeSet): LineRangeSet {
const result: LineRange[] = [];
let i1 = 0;
let i2 = 0;
while (i1 < this._normalizedRanges.length && i2 < other._normalizedRanges.length) {
const r1 = this._normalizedRanges[i1];
const r2 = other._normalizedRanges[i2];
const i = r1.intersect(r2);
if (i && !i.isEmpty) {
result.push(i);
}
if (r1.endLineNumberExclusive < r2.endLineNumberExclusive) {
i1++;
} else {
i2++;
}
}
return new LineRangeSet(result);
}
getWithDelta(value: number): LineRangeSet {
return new LineRangeSet(this._normalizedRanges.map(r => r.delta(value)));
}
}
+14
View File
@@ -136,6 +136,20 @@ export class OffsetRange {
}
return value;
}
public map<T>(f: (offset: number) => T): T[] {
const result: T[] = [];
for (let i = this.start; i < this.endExclusive; i++) {
result.push(f(i));
}
return result;
}
public forEach(f: (offset: number) => void): void {
for (let i = this.start; i < this.endExclusive; i++) {
f(i);
}
}
}
export class OffsetRangeSet {
@@ -3,7 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { compareBy, findLastMaxBy, findMinBy } from 'vs/base/common/arrays';
import { compareBy } from 'vs/base/common/arrays';
import { findLastMaxBy, findFirstMinBy } from 'vs/base/common/arraysFind';
import { CursorState, PartialCursorState } from 'vs/editor/common/cursorCommon';
import { CursorContext } from 'vs/editor/common/cursor/cursorContext';
import { Cursor } from 'vs/editor/common/cursor/oneCursor';
@@ -72,7 +73,7 @@ export class CursorCollection {
}
public getTopMostViewPosition(): Position {
return findMinBy(
return findFirstMinBy(
this.cursors,
compareBy(c => c.viewState.position, Position.compare)
)!.viewState.position;
File diff suppressed because it is too large Load Diff
@@ -1,20 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class Array2D<T> {
private readonly array: T[] = [];
constructor(public readonly width: number, public readonly height: number) {
this.array = new Array<T>(width * height);
}
get(x: number, y: number): T {
return this.array[x + y * this.width];
}
set(x: number, y: number, value: T): void {
this.array[x + y * this.width] = value;
}
}
@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { forEachAdjacent } from 'vs/base/common/arrays';
import { BugIndicatingError } from 'vs/base/common/errors';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
@@ -33,12 +34,29 @@ export class DiffAlgorithmResult {
}
export class SequenceDiff {
public static invert(sequenceDiffs: SequenceDiff[], doc1Length: number): SequenceDiff[] {
const result: SequenceDiff[] = [];
forEachAdjacent(sequenceDiffs, (a, b) => {
const seq1Start = a ? a.seq1Range.endExclusive : 0;
const seq2Start = a ? a.seq2Range.endExclusive : 0;
const seq1EndEx = b ? b.seq1Range.start : doc1Length;
const seq2EndEx = b ? b.seq2Range.start : (a ? a.seq2Range.endExclusive - a.seq1Range.endExclusive : 0) + doc1Length;
result.push(new SequenceDiff(
new OffsetRange(seq1Start, seq1EndEx),
new OffsetRange(seq2Start, seq2EndEx),
));
});
return result;
}
constructor(
public readonly seq1Range: OffsetRange,
public readonly seq2Range: OffsetRange,
) { }
public reverse(): SequenceDiff {
public swap(): SequenceDiff {
return new SequenceDiff(this.seq2Range, this.seq1Range);
}
@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { IDiffAlgorithm, SequenceDiff, ISequence, ITimeout, InfiniteTimeout, DiffAlgorithmResult } from 'vs/editor/common/diff/algorithms/diffAlgorithm';
import { Array2D } from 'vs/editor/common/diff/algorithms/utils';
import { IDiffAlgorithm, SequenceDiff, ISequence, ITimeout, InfiniteTimeout, DiffAlgorithmResult } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm';
import { Array2D } from 'vs/editor/common/diff/defaultLinesDiffComputer/utils';
/**
* A O(MN) diffing algorithm that supports a score function.
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { DiffAlgorithmResult, IDiffAlgorithm, ISequence, ITimeout, InfiniteTimeout, SequenceDiff } from 'vs/editor/common/diff/algorithms/diffAlgorithm';
import { DiffAlgorithmResult, IDiffAlgorithm, ISequence, ITimeout, InfiniteTimeout, SequenceDiff } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm';
/**
* An O(ND) diff algorithm that has a quadratic space worst-case complexity.
@@ -0,0 +1,314 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITimeout, SequenceDiff } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm';
import { DetailedLineRangeMapping, LineRangeMapping } from '../rangeMapping';
import { pushMany, compareBy, numberComparator, reverseOrder } from 'vs/base/common/arrays';
import { MonotonousArray, findLastMonotonous } from 'vs/base/common/arraysFind';
import { SetMap } from 'vs/base/common/collections';
import { LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence';
import { LineRangeFragment, isSpace } from 'vs/editor/common/diff/defaultLinesDiffComputer/utils';
import { MyersDiffAlgorithm } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm';
export function computeMovedLines(
changes: DetailedLineRangeMapping[],
originalLines: string[],
modifiedLines: string[],
hashedOriginalLines: number[],
hashedModifiedLines: number[],
timeout: ITimeout
): LineRangeMapping[] {
let { moves, excludedChanges } = computeMovesFromSimpleDeletionsToSimpleInsertions(changes, originalLines, modifiedLines, timeout);
if (!timeout.isValid()) { return []; }
const filteredChanges = changes.filter(c => !excludedChanges.has(c));
const unchangedMoves = computeUnchangedMoves(filteredChanges, hashedOriginalLines, hashedModifiedLines, originalLines, modifiedLines, timeout);
pushMany(moves, unchangedMoves);
moves = joinCloseConsecutiveMoves(moves);
// Ignore too short moves
moves = moves.filter(current => {
const originalText = current.original.toOffsetRange().slice(originalLines).map(l => l.trim()).join('\n');
return originalText.length >= 10;
});
moves = removeMovesInSameDiff(changes, moves);
return moves;
}
function computeMovesFromSimpleDeletionsToSimpleInsertions(
changes: DetailedLineRangeMapping[],
originalLines: string[],
modifiedLines: string[],
timeout: ITimeout,
) {
const moves: LineRangeMapping[] = [];
const deletions = changes
.filter(c => c.modified.isEmpty && c.original.length >= 3)
.map(d => new LineRangeFragment(d.original, originalLines, d));
const insertions = new Set(changes
.filter(c => c.original.isEmpty && c.modified.length >= 3)
.map(d => new LineRangeFragment(d.modified, modifiedLines, d)));
const excludedChanges = new Set<DetailedLineRangeMapping>();
for (const deletion of deletions) {
let highestSimilarity = -1;
let best: LineRangeFragment | undefined;
for (const insertion of insertions) {
const similarity = deletion.computeSimilarity(insertion);
if (similarity > highestSimilarity) {
highestSimilarity = similarity;
best = insertion;
}
}
if (highestSimilarity > 0.90 && best) {
insertions.delete(best);
moves.push(new LineRangeMapping(deletion.range, best.range));
excludedChanges.add(deletion.source);
excludedChanges.add(best.source);
}
if (!timeout.isValid()) {
return { moves, excludedChanges };
}
}
return { moves, excludedChanges };
}
function computeUnchangedMoves(
changes: DetailedLineRangeMapping[],
hashedOriginalLines: number[],
hashedModifiedLines: number[],
originalLines: string[],
modifiedLines: string[],
timeout: ITimeout,
) {
const moves: LineRangeMapping[] = [];
const original3LineHashes = new SetMap<string, { range: LineRange }>();
for (const change of changes) {
for (let i = change.original.startLineNumber; i < change.original.endLineNumberExclusive - 2; i++) {
const key = `${hashedOriginalLines[i - 1]}:${hashedOriginalLines[i + 1 - 1]}:${hashedOriginalLines[i + 2 - 1]}`;
original3LineHashes.add(key, { range: new LineRange(i, i + 3) });
}
}
interface PossibleMapping {
modifiedLineRange: LineRange;
originalLineRange: LineRange;
}
const possibleMappings: PossibleMapping[] = [];
changes.sort(compareBy(c => c.modified.startLineNumber, numberComparator));
for (const change of changes) {
let lastMappings: PossibleMapping[] = [];
for (let i = change.modified.startLineNumber; i < change.modified.endLineNumberExclusive - 2; i++) {
const key = `${hashedModifiedLines[i - 1]}:${hashedModifiedLines[i + 1 - 1]}:${hashedModifiedLines[i + 2 - 1]}`;
const currentModifiedRange = new LineRange(i, i + 3);
const nextMappings: PossibleMapping[] = [];
original3LineHashes.forEach(key, ({ range }) => {
for (const lastMapping of lastMappings) {
// does this match extend some last match?
if (lastMapping.originalLineRange.endLineNumberExclusive + 1 === range.endLineNumberExclusive &&
lastMapping.modifiedLineRange.endLineNumberExclusive + 1 === currentModifiedRange.endLineNumberExclusive) {
lastMapping.originalLineRange = new LineRange(lastMapping.originalLineRange.startLineNumber, range.endLineNumberExclusive);
lastMapping.modifiedLineRange = new LineRange(lastMapping.modifiedLineRange.startLineNumber, currentModifiedRange.endLineNumberExclusive);
nextMappings.push(lastMapping);
return;
}
}
const mapping: PossibleMapping = {
modifiedLineRange: currentModifiedRange,
originalLineRange: range,
};
possibleMappings.push(mapping);
nextMappings.push(mapping);
});
lastMappings = nextMappings;
}
if (!timeout.isValid()) {
return [];
}
}
possibleMappings.sort(reverseOrder(compareBy(m => m.modifiedLineRange.length, numberComparator)));
const modifiedSet = new LineRangeSet();
const originalSet = new LineRangeSet();
for (const mapping of possibleMappings) {
const diffOrigToMod = mapping.modifiedLineRange.startLineNumber - mapping.originalLineRange.startLineNumber;
const modifiedSections = modifiedSet.subtractFrom(mapping.modifiedLineRange);
const originalTranslatedSections = originalSet.subtractFrom(mapping.originalLineRange).getWithDelta(diffOrigToMod);
const modifiedIntersectedSections = modifiedSections.getIntersection(originalTranslatedSections);
for (const s of modifiedIntersectedSections.ranges) {
if (s.length < 3) {
continue;
}
const modifiedLineRange = s;
const originalLineRange = s.delta(-diffOrigToMod);
moves.push(new LineRangeMapping(originalLineRange, modifiedLineRange));
modifiedSet.addRange(modifiedLineRange);
originalSet.addRange(originalLineRange);
}
}
moves.sort(compareBy(m => m.original.startLineNumber, numberComparator));
const monotonousChanges = new MonotonousArray(changes);
for (let i = 0; i < moves.length; i++) {
const move = moves[i];
const firstTouchingChangeOrig = monotonousChanges.findLastMonotonous(c => c.original.startLineNumber <= move.original.startLineNumber)!;
const firstTouchingChangeMod = findLastMonotonous(changes, c => c.modified.startLineNumber <= move.modified.startLineNumber)!;
const linesAbove = Math.max(
move.original.startLineNumber - firstTouchingChangeOrig.original.startLineNumber,
move.modified.startLineNumber - firstTouchingChangeMod.modified.startLineNumber
);
const lastTouchingChangeOrig = monotonousChanges.findLastMonotonous(c => c.original.startLineNumber < move.original.endLineNumberExclusive)!;
const lastTouchingChangeMod = findLastMonotonous(changes, c => c.modified.startLineNumber < move.modified.endLineNumberExclusive)!;
const linesBelow = Math.max(
lastTouchingChangeOrig.original.endLineNumberExclusive - move.original.endLineNumberExclusive,
lastTouchingChangeMod.modified.endLineNumberExclusive - move.modified.endLineNumberExclusive
);
let extendToTop: number;
for (extendToTop = 0; extendToTop < linesAbove; extendToTop++) {
const origLine = move.original.startLineNumber - extendToTop - 1;
const modLine = move.modified.startLineNumber - extendToTop - 1;
if (modifiedSet.contains(modLine) || originalSet.contains(origLine)) {
break;
}
if (!areLinesSimilar(originalLines[origLine - 1], modifiedLines[modLine - 1], timeout)) {
break;
}
}
if (extendToTop > 0) {
originalSet.addRange(new LineRange(move.original.startLineNumber - extendToTop, move.original.startLineNumber));
modifiedSet.addRange(new LineRange(move.modified.startLineNumber - extendToTop, move.modified.startLineNumber));
}
let extendToBottom: number;
for (extendToBottom = 0; extendToBottom < linesBelow; extendToBottom++) {
const origLine = move.original.endLineNumberExclusive + extendToBottom;
const modLine = move.modified.endLineNumberExclusive + extendToBottom;
if (modifiedSet.contains(modLine) || originalSet.contains(origLine)) {
break;
}
if (!areLinesSimilar(originalLines[origLine - 1], modifiedLines[modLine - 1], timeout)) {
break;
}
}
if (extendToBottom > 0) {
originalSet.addRange(new LineRange(move.original.endLineNumberExclusive, move.original.endLineNumberExclusive + extendToBottom));
modifiedSet.addRange(new LineRange(move.modified.endLineNumberExclusive, move.modified.endLineNumberExclusive + extendToBottom));
}
if (extendToTop > 0 || extendToBottom > 0) {
moves[i] = new LineRangeMapping(
new LineRange(move.original.startLineNumber - extendToTop, move.original.endLineNumberExclusive + extendToBottom),
new LineRange(move.modified.startLineNumber - extendToTop, move.modified.endLineNumberExclusive + extendToBottom),
);
}
}
return moves;
}
function areLinesSimilar(line1: string, line2: string, timeout: ITimeout): boolean {
if (line1.trim() === line2.trim()) { return true; }
if (line1.length > 300 && line2.length > 300) { return false; }
const myersDiffingAlgorithm = new MyersDiffAlgorithm();
const result = myersDiffingAlgorithm.compute(
new LinesSliceCharSequence([line1], new OffsetRange(0, 1), false),
new LinesSliceCharSequence([line2], new OffsetRange(0, 1), false),
timeout
);
let commonNonSpaceCharCount = 0;
const inverted = SequenceDiff.invert(result.diffs, line1.length);
for (const seq of inverted) {
seq.seq1Range.forEach(idx => {
if (!isSpace(line1.charCodeAt(idx))) {
commonNonSpaceCharCount++;
}
});
}
function countNonWsChars(str: string): number {
let count = 0;
for (let i = 0; i < line1.length; i++) {
if (!isSpace(str.charCodeAt(i))) {
count++;
}
}
return count;
}
const longerLineLength = countNonWsChars(line1.length > line2.length ? line1 : line2);
const r = commonNonSpaceCharCount / longerLineLength > 0.6 && longerLineLength > 10;
return r;
}
function joinCloseConsecutiveMoves(moves: LineRangeMapping[]): LineRangeMapping[] {
if (moves.length === 0) {
return moves;
}
moves.sort(compareBy(m => m.original.startLineNumber, numberComparator));
const result = [moves[0]];
for (let i = 1; i < moves.length; i++) {
const last = result[result.length - 1];
const current = moves[i];
const originalDist = current.original.startLineNumber - last.original.endLineNumberExclusive;
const modifiedDist = current.modified.startLineNumber - last.modified.endLineNumberExclusive;
const currentMoveAfterLast = originalDist >= 0 && modifiedDist >= 0;
if (currentMoveAfterLast && originalDist + modifiedDist <= 2) {
result[result.length - 1] = last.join(current);
continue;
}
result.push(current);
}
return result;
}
function removeMovesInSameDiff(changes: DetailedLineRangeMapping[], moves: LineRangeMapping[]) {
const changesMonotonous = new MonotonousArray(changes);
moves = moves.filter(m => {
const diffBeforeOriginalMove = changesMonotonous.findLastMonotonous(c => c.original.endLineNumberExclusive <= m.original.startLineNumber)
|| new LineRangeMapping(new LineRange(1, 1), new LineRange(1, 1));
const modifiedDistToPrevDiff = m.modified.startLineNumber - diffBeforeOriginalMove.modified.endLineNumberExclusive;
const originalDistToPrevDiff = m.original.startLineNumber - diffBeforeOriginalMove.original.endLineNumberExclusive;
const differentDistances = modifiedDistToPrevDiff !== originalDistToPrevDiff;
return differentDistances;
});
return moves;
}
@@ -0,0 +1,310 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { equals, groupAdjacentBy } from 'vs/base/common/arrays';
import { assertFn, checkAdjacentItems } from 'vs/base/common/assert';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { DateTimeout, ITimeout, InfiniteTimeout, SequenceDiff } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm';
import { DynamicProgrammingDiffing } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/dynamicProgrammingDiffing';
import { MyersDiffAlgorithm } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm';
import { computeMovedLines } from 'vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines';
import { extendDiffsToEntireWordIfAppropriate, optimizeSequenceDiffs, removeVeryShortMatchingLinesBetweenDiffs, removeVeryShortMatchingTextBetweenLongDiffs, removeShortMatches } from 'vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations';
import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from 'vs/editor/common/diff/linesDiffComputer';
import { DetailedLineRangeMapping, RangeMapping } from '../rangeMapping';
import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence';
import { LineSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/lineSequence';
export class DefaultLinesDiffComputer implements ILinesDiffComputer {
private readonly dynamicProgrammingDiffing = new DynamicProgrammingDiffing();
private readonly myersDiffingAlgorithm = new MyersDiffAlgorithm();
computeDiff(originalLines: string[], modifiedLines: string[], options: ILinesDiffComputerOptions): LinesDiff {
if (originalLines.length <= 1 && equals(originalLines, modifiedLines, (a, b) => a === b)) {
return new LinesDiff([], [], false);
}
if (originalLines.length === 1 && originalLines[0].length === 0 || modifiedLines.length === 1 && modifiedLines[0].length === 0) {
return new LinesDiff([
new DetailedLineRangeMapping(
new LineRange(1, originalLines.length + 1),
new LineRange(1, modifiedLines.length + 1),
[
new RangeMapping(
new Range(1, 1, originalLines.length, originalLines[0].length + 1),
new Range(1, 1, modifiedLines.length, modifiedLines[0].length + 1)
)
]
)
], [], false);
}
const timeout = options.maxComputationTimeMs === 0 ? InfiniteTimeout.instance : new DateTimeout(options.maxComputationTimeMs);
const considerWhitespaceChanges = !options.ignoreTrimWhitespace;
const perfectHashes = new Map<string, number>();
function getOrCreateHash(text: string): number {
let hash = perfectHashes.get(text);
if (hash === undefined) {
hash = perfectHashes.size;
perfectHashes.set(text, hash);
}
return hash;
}
const originalLinesHashes = originalLines.map((l) => getOrCreateHash(l.trim()));
const modifiedLinesHashes = modifiedLines.map((l) => getOrCreateHash(l.trim()));
const sequence1 = new LineSequence(originalLinesHashes, originalLines);
const sequence2 = new LineSequence(modifiedLinesHashes, modifiedLines);
const lineAlignmentResult = (() => {
if (sequence1.length + sequence2.length < 1700) {
// Use the improved algorithm for small files
return this.dynamicProgrammingDiffing.compute(
sequence1,
sequence2,
timeout,
(offset1, offset2) =>
originalLines[offset1] === modifiedLines[offset2]
? modifiedLines[offset2].length === 0
? 0.1
: 1 + Math.log(1 + modifiedLines[offset2].length)
: 0.99
);
}
return this.myersDiffingAlgorithm.compute(
sequence1,
sequence2
);
})();
let lineAlignments = lineAlignmentResult.diffs;
let hitTimeout = lineAlignmentResult.hitTimeout;
lineAlignments = optimizeSequenceDiffs(sequence1, sequence2, lineAlignments);
lineAlignments = removeVeryShortMatchingLinesBetweenDiffs(sequence1, sequence2, lineAlignments);
const alignments: RangeMapping[] = [];
const scanForWhitespaceChanges = (equalLinesCount: number) => {
if (!considerWhitespaceChanges) {
return;
}
for (let i = 0; i < equalLinesCount; i++) {
const seq1Offset = seq1LastStart + i;
const seq2Offset = seq2LastStart + i;
if (originalLines[seq1Offset] !== modifiedLines[seq2Offset]) {
// This is because of whitespace changes, diff these lines
const characterDiffs = this.refineDiff(originalLines, modifiedLines, new SequenceDiff(
new OffsetRange(seq1Offset, seq1Offset + 1),
new OffsetRange(seq2Offset, seq2Offset + 1),
), timeout, considerWhitespaceChanges);
for (const a of characterDiffs.mappings) {
alignments.push(a);
}
if (characterDiffs.hitTimeout) {
hitTimeout = true;
}
}
}
};
let seq1LastStart = 0;
let seq2LastStart = 0;
for (const diff of lineAlignments) {
assertFn(() => diff.seq1Range.start - seq1LastStart === diff.seq2Range.start - seq2LastStart);
const equalLinesCount = diff.seq1Range.start - seq1LastStart;
scanForWhitespaceChanges(equalLinesCount);
seq1LastStart = diff.seq1Range.endExclusive;
seq2LastStart = diff.seq2Range.endExclusive;
const characterDiffs = this.refineDiff(originalLines, modifiedLines, diff, timeout, considerWhitespaceChanges);
if (characterDiffs.hitTimeout) {
hitTimeout = true;
}
for (const a of characterDiffs.mappings) {
alignments.push(a);
}
}
scanForWhitespaceChanges(originalLines.length - seq1LastStart);
const changes = lineRangeMappingFromRangeMappings(alignments, originalLines, modifiedLines);
let moves: MovedText[] = [];
if (options.computeMoves) {
moves = this.computeMoves(changes, originalLines, modifiedLines, originalLinesHashes, modifiedLinesHashes, timeout, considerWhitespaceChanges);
}
// Make sure all ranges are valid
assertFn(() => {
function validatePosition(pos: Position, lines: string[]): boolean {
if (pos.lineNumber < 1 || pos.lineNumber > lines.length) { return false; }
const line = lines[pos.lineNumber - 1];
if (pos.column < 1 || pos.column > line.length + 1) { return false; }
return true;
}
function validateRange(range: LineRange, lines: string[]): boolean {
if (range.startLineNumber < 1 || range.startLineNumber > lines.length + 1) { return false; }
if (range.endLineNumberExclusive < 1 || range.endLineNumberExclusive > lines.length + 1) { return false; }
return true;
}
for (const c of changes) {
if (!c.innerChanges) { return false; }
for (const ic of c.innerChanges) {
const valid = validatePosition(ic.modifiedRange.getStartPosition(), modifiedLines) && validatePosition(ic.modifiedRange.getEndPosition(), modifiedLines) &&
validatePosition(ic.originalRange.getStartPosition(), originalLines) && validatePosition(ic.originalRange.getEndPosition(), originalLines);
if (!valid) { return false; }
}
if (!validateRange(c.modified, modifiedLines) || !validateRange(c.original, originalLines)) {
return false;
}
}
return true;
});
return new LinesDiff(changes, moves, hitTimeout);
}
private computeMoves(
changes: DetailedLineRangeMapping[],
originalLines: string[],
modifiedLines: string[],
hashedOriginalLines: number[],
hashedModifiedLines: number[],
timeout: ITimeout,
considerWhitespaceChanges: boolean,
): MovedText[] {
const moves = computeMovedLines(
changes,
originalLines,
modifiedLines,
hashedOriginalLines,
hashedModifiedLines,
timeout,
);
const movesWithDiffs = moves.map(m => {
const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff(
m.original.toOffsetRange(),
m.modified.toOffsetRange(),
), timeout, considerWhitespaceChanges);
const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true);
return new MovedText(m, mappings);
});
return movesWithDiffs;
}
private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } {
const slice1 = new LinesSliceCharSequence(originalLines, diff.seq1Range, considerWhitespaceChanges);
const slice2 = new LinesSliceCharSequence(modifiedLines, diff.seq2Range, considerWhitespaceChanges);
const diffResult = slice1.length + slice2.length < 500
? this.dynamicProgrammingDiffing.compute(slice1, slice2, timeout)
: this.myersDiffingAlgorithm.compute(slice1, slice2, timeout);
let diffs = diffResult.diffs;
diffs = optimizeSequenceDiffs(slice1, slice2, diffs);
diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs);
diffs = removeShortMatches(slice1, slice2, diffs);
diffs = removeVeryShortMatchingTextBetweenLongDiffs(slice1, slice2, diffs);
const result = diffs.map(
(d) =>
new RangeMapping(
slice1.translateRange(d.seq1Range),
slice2.translateRange(d.seq2Range)
)
);
// Assert: result applied on original should be the same as diff applied to original
return {
mappings: result,
hitTimeout: diffResult.hitTimeout,
};
}
}
export function lineRangeMappingFromRangeMappings(alignments: RangeMapping[], originalLines: string[], modifiedLines: string[], dontAssertStartLine: boolean = false): DetailedLineRangeMapping[] {
const changes: DetailedLineRangeMapping[] = [];
for (const g of groupAdjacentBy(
alignments.map(a => getLineRangeMapping(a, originalLines, modifiedLines)),
(a1, a2) =>
a1.original.overlapOrTouch(a2.original)
|| a1.modified.overlapOrTouch(a2.modified)
)) {
const first = g[0];
const last = g[g.length - 1];
changes.push(new DetailedLineRangeMapping(
first.original.join(last.original),
first.modified.join(last.modified),
g.map(a => a.innerChanges![0]),
));
}
assertFn(() => {
if (!dontAssertStartLine) {
if (changes.length > 0 && changes[0].original.startLineNumber !== changes[0].modified.startLineNumber) {
return false;
}
}
return checkAdjacentItems(changes,
(m1, m2) => m2.original.startLineNumber - m1.original.endLineNumberExclusive === m2.modified.startLineNumber - m1.modified.endLineNumberExclusive &&
// There has to be an unchanged line in between (otherwise both diffs should have been joined)
m1.original.endLineNumberExclusive < m2.original.startLineNumber &&
m1.modified.endLineNumberExclusive < m2.modified.startLineNumber,
);
});
return changes;
}
export function getLineRangeMapping(rangeMapping: RangeMapping, originalLines: string[], modifiedLines: string[]): DetailedLineRangeMapping {
let lineStartDelta = 0;
let lineEndDelta = 0;
// rangeMapping describes the edit that replaces `rangeMapping.originalRange` with `newText := getText(modifiedLines, rangeMapping.modifiedRange)`.
// original: ]xxx \n <- this line is not modified
// modified: ]xx \n
if (rangeMapping.modifiedRange.endColumn === 1 && rangeMapping.originalRange.endColumn === 1
&& rangeMapping.originalRange.startLineNumber + lineStartDelta <= rangeMapping.originalRange.endLineNumber
&& rangeMapping.modifiedRange.startLineNumber + lineStartDelta <= rangeMapping.modifiedRange.endLineNumber) {
// We can only do this if the range is not empty yet
lineEndDelta = -1;
}
// original: xxx[ \n <- this line is not modified
// modified: xxx[ \n
if (rangeMapping.modifiedRange.startColumn - 1 >= modifiedLines[rangeMapping.modifiedRange.startLineNumber - 1].length
&& rangeMapping.originalRange.startColumn - 1 >= originalLines[rangeMapping.originalRange.startLineNumber - 1].length
&& rangeMapping.originalRange.startLineNumber <= rangeMapping.originalRange.endLineNumber + lineEndDelta
&& rangeMapping.modifiedRange.startLineNumber <= rangeMapping.modifiedRange.endLineNumber + lineEndDelta) {
// We can only do this if the range is not empty yet
lineStartDelta = 1;
}
const originalLineRange = new LineRange(
rangeMapping.originalRange.startLineNumber + lineStartDelta,
rangeMapping.originalRange.endLineNumber + 1 + lineEndDelta
);
const modifiedLineRange = new LineRange(
rangeMapping.modifiedRange.startLineNumber + lineStartDelta,
rangeMapping.modifiedRange.endLineNumber + 1 + lineEndDelta
);
return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, [rangeMapping]);
}
@@ -4,177 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { ISequence, SequenceDiff } from 'vs/editor/common/diff/algorithms/diffAlgorithm';
import { LineSequence, LinesSliceCharSequence } from 'vs/editor/common/diff/advancedLinesDiffComputer';
import { ISequence, SequenceDiff } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm';
import { LineSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/lineSequence';
import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence';
export function optimizeSequenceDiffs(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
let result = sequenceDiffs;
result = joinSequenceDiffs(sequence1, sequence2, result);
result = joinSequenceDiffsByShifting(sequence1, sequence2, result);
result = shiftSequenceDiffs(sequence1, sequence2, result);
return result;
}
export function smoothenSequenceDiffs(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
const result: SequenceDiff[] = [];
for (const s of sequenceDiffs) {
const last = result[result.length - 1];
if (!last) {
result.push(s);
continue;
}
if (s.seq1Range.start - last.seq1Range.endExclusive <= 2 || s.seq2Range.start - last.seq2Range.endExclusive <= 2) {
result[result.length - 1] = new SequenceDiff(last.seq1Range.join(s.seq1Range), last.seq2Range.join(s.seq2Range));
} else {
result.push(s);
}
}
return result;
}
export function removeRandomLineMatches(sequence1: LineSequence, _sequence2: LineSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
let diffs = sequenceDiffs;
if (diffs.length === 0) {
return diffs;
}
let counter = 0;
let shouldRepeat: boolean;
do {
shouldRepeat = false;
const result: SequenceDiff[] = [
diffs[0]
];
for (let i = 1; i < diffs.length; i++) {
const cur = diffs[i];
const lastResult = result[result.length - 1];
function shouldJoinDiffs(before: SequenceDiff, after: SequenceDiff): boolean {
const unchangedRange = new OffsetRange(lastResult.seq1Range.endExclusive, cur.seq1Range.start);
const unchangedText = sequence1.getText(unchangedRange);
const unchangedTextWithoutWs = unchangedText.replace(/\s/g, '');
if (unchangedTextWithoutWs.length <= 4
&& (before.seq1Range.length + before.seq2Range.length > 5 || after.seq1Range.length + after.seq2Range.length > 5)) {
return true;
}
return false;
}
const shouldJoin = shouldJoinDiffs(lastResult, cur);
if (shouldJoin) {
shouldRepeat = true;
result[result.length - 1] = result[result.length - 1].join(cur);
} else {
result.push(cur);
}
}
diffs = result;
} while (counter++ < 10 && shouldRepeat);
return diffs;
}
export function removeRandomMatches(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
let diffs = sequenceDiffs;
if (diffs.length === 0) {
return diffs;
}
let counter = 0;
let shouldRepeat: boolean;
do {
shouldRepeat = false;
const result: SequenceDiff[] = [
diffs[0]
];
for (let i = 1; i < diffs.length; i++) {
const cur = diffs[i];
const lastResult = result[result.length - 1];
function shouldJoinDiffs(before: SequenceDiff, after: SequenceDiff): boolean {
const unchangedRange = new OffsetRange(lastResult.seq1Range.endExclusive, cur.seq1Range.start);
const unchangedLineCount = sequence1.countLinesIn(unchangedRange);
if (unchangedLineCount > 5 || unchangedRange.length > 500) {
return false;
}
const unchangedText = sequence1.getText(unchangedRange).trim();
if (unchangedText.length > 20 || unchangedText.split(/\r\n|\r|\n/).length > 1) {
return false;
}
const beforeLineCount1 = sequence1.countLinesIn(before.seq1Range);
const beforeSeq1Length = before.seq1Range.length;
const beforeLineCount2 = sequence2.countLinesIn(before.seq2Range);
const beforeSeq2Length = before.seq2Range.length;
const afterLineCount1 = sequence1.countLinesIn(after.seq1Range);
const afterSeq1Length = after.seq1Range.length;
const afterLineCount2 = sequence2.countLinesIn(after.seq2Range);
const afterSeq2Length = after.seq2Range.length;
// TODO: Maybe a neural net can be used to derive the result from these numbers
const max = 2 * 40 + 50;
function cap(v: number): number {
return Math.min(v, max);
}
if (Math.pow(Math.pow(cap(beforeLineCount1 * 40 + beforeSeq1Length), 1.5) + Math.pow(cap(beforeLineCount2 * 40 + beforeSeq2Length), 1.5), 1.5)
+ Math.pow(Math.pow(cap(afterLineCount1 * 40 + afterSeq1Length), 1.5) + Math.pow(cap(afterLineCount2 * 40 + afterSeq2Length), 1.5), 1.5) > ((max ** 1.5) ** 1.5) * 1.3) {
return true;
}
return false;
}
const shouldJoin = shouldJoinDiffs(lastResult, cur);
if (shouldJoin) {
shouldRepeat = true;
result[result.length - 1] = result[result.length - 1].join(cur);
} else {
result.push(cur);
}
}
diffs = result;
} while (counter++ < 10 && shouldRepeat);
// Remove short suffixes/prefixes
for (let i = 0; i < diffs.length; i++) {
const cur = diffs[i];
let range1 = cur.seq1Range;
let range2 = cur.seq2Range;
const fullRange1 = sequence1.extendToFullLines(cur.seq1Range);
const prefix = sequence1.getText(new OffsetRange(fullRange1.start, cur.seq1Range.start));
if (prefix.length > 0 && prefix.trim().length <= 3 && cur.seq1Range.length + cur.seq2Range.length > 100) {
range1 = cur.seq1Range.deltaStart(-prefix.length);
range2 = cur.seq2Range.deltaStart(-prefix.length);
}
const suffix = sequence1.getText(new OffsetRange(cur.seq1Range.endExclusive, fullRange1.endExclusive));
if (suffix.length > 0 && (suffix.trim().length <= 3 && cur.seq1Range.length + cur.seq2Range.length > 150)) {
range1 = range1.deltaEnd(suffix.length);
range2 = range2.deltaEnd(suffix.length);
}
diffs[i] = new SequenceDiff(range1, range2);
}
return diffs;
}
/**
* This function fixes issues like this:
* ```
@@ -187,7 +27,7 @@ export function removeRandomMatches(sequence1: LinesSliceCharSequence, sequence2
* Computed diff: [ {Add "," after Bar}, {Add "Foo " after space} }
* Improved diff: [{Add ", Foo" after Bar}]
*/
export function joinSequenceDiffs(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
function joinSequenceDiffsByShifting(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
if (sequenceDiffs.length === 0) {
return sequenceDiffs;
}
@@ -285,7 +125,7 @@ export function joinSequenceDiffs(sequence1: ISequence, sequence2: ISequence, se
// ->
// collectBrackets(level + 1, [levelPerBracket + 1, ]levelPerBracketType);
export function shiftSequenceDiffs(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
function shiftSequenceDiffs(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
if (!sequence1.getBoundaryScore || !sequence2.getBoundaryScore) {
return sequenceDiffs;
}
@@ -301,7 +141,7 @@ export function shiftSequenceDiffs(sequence1: ISequence, sequence2: ISequence, s
if (diff.seq1Range.isEmpty) {
sequenceDiffs[i] = shiftDiffToBetterPosition(diff, sequence1, sequence2, seq1ValidRange, seq2ValidRange);
} else if (diff.seq2Range.isEmpty) {
sequenceDiffs[i] = shiftDiffToBetterPosition(diff.reverse(), sequence2, sequence1, seq2ValidRange, seq1ValidRange).reverse();
sequenceDiffs[i] = shiftDiffToBetterPosition(diff.swap(), sequence2, sequence1, seq2ValidRange, seq1ValidRange).swap();
}
}
@@ -355,3 +195,258 @@ function shiftDiffToBetterPosition(diff: SequenceDiff, sequence1: ISequence, seq
return diff.delta(bestDelta);
}
export function removeShortMatches(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
const result: SequenceDiff[] = [];
for (const s of sequenceDiffs) {
const last = result[result.length - 1];
if (!last) {
result.push(s);
continue;
}
if (s.seq1Range.start - last.seq1Range.endExclusive <= 2 || s.seq2Range.start - last.seq2Range.endExclusive <= 2) {
result[result.length - 1] = new SequenceDiff(last.seq1Range.join(s.seq1Range), last.seq2Range.join(s.seq2Range));
} else {
result.push(s);
}
}
return result;
}
export function extendDiffsToEntireWordIfAppropriate(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
const additional: SequenceDiff[] = [];
let lastModifiedWord: { added: number; deleted: number; count: number; s1Range: OffsetRange; s2Range: OffsetRange } | undefined = undefined;
function maybePushWordToAdditional() {
if (!lastModifiedWord) {
return;
}
const originalLength1 = lastModifiedWord.s1Range.length - lastModifiedWord.deleted;
const originalLength2 = lastModifiedWord.s2Range.length - lastModifiedWord.added;
if (originalLength1 !== originalLength2) {
// TODO figure out why this happens
}
if (Math.max(lastModifiedWord.deleted, lastModifiedWord.added) + (lastModifiedWord.count - 1) > originalLength1) {
additional.push(new SequenceDiff(lastModifiedWord.s1Range, lastModifiedWord.s2Range));
}
lastModifiedWord = undefined;
}
for (const s of sequenceDiffs) {
function processWord(s1Range: OffsetRange, s2Range: OffsetRange) {
if (!lastModifiedWord || !lastModifiedWord.s1Range.containsRange(s1Range) || !lastModifiedWord.s2Range.containsRange(s2Range)) {
if (lastModifiedWord && !(lastModifiedWord.s1Range.endExclusive < s1Range.start && lastModifiedWord.s2Range.endExclusive < s2Range.start)) {
const s1Added = OffsetRange.tryCreate(lastModifiedWord.s1Range.endExclusive, s1Range.start);
const s2Added = OffsetRange.tryCreate(lastModifiedWord.s2Range.endExclusive, s2Range.start);
lastModifiedWord.deleted += s1Added?.length ?? 0;
lastModifiedWord.added += s2Added?.length ?? 0;
lastModifiedWord.s1Range = lastModifiedWord.s1Range.join(s1Range);
lastModifiedWord.s2Range = lastModifiedWord.s2Range.join(s2Range);
} else {
maybePushWordToAdditional();
lastModifiedWord = { added: 0, deleted: 0, count: 0, s1Range: s1Range, s2Range: s2Range };
}
}
const changedS1 = s1Range.intersect(s.seq1Range);
const changedS2 = s2Range.intersect(s.seq2Range);
lastModifiedWord.count++;
lastModifiedWord.deleted += changedS1?.length ?? 0;
lastModifiedWord.added += changedS2?.length ?? 0;
}
const w1Before = sequence1.findWordContaining(s.seq1Range.start - 1);
const w2Before = sequence2.findWordContaining(s.seq2Range.start - 1);
const w1After = sequence1.findWordContaining(s.seq1Range.endExclusive);
const w2After = sequence2.findWordContaining(s.seq2Range.endExclusive);
if (w1Before && w1After && w2Before && w2After && w1Before.equals(w1After) && w2Before.equals(w2After)) {
processWord(w1Before, w2Before);
} else {
if (w1Before && w2Before) {
processWord(w1Before, w2Before);
}
if (w1After && w2After) {
processWord(w1After, w2After);
}
}
}
maybePushWordToAdditional();
const merged = mergeSequenceDiffs(sequenceDiffs, additional);
return merged;
}
function mergeSequenceDiffs(sequenceDiffs1: SequenceDiff[], sequenceDiffs2: SequenceDiff[]): SequenceDiff[] {
const result: SequenceDiff[] = [];
while (sequenceDiffs1.length > 0 || sequenceDiffs2.length > 0) {
const sd1 = sequenceDiffs1[0];
const sd2 = sequenceDiffs2[0];
let next: SequenceDiff;
if (sd1 && (!sd2 || sd1.seq1Range.start < sd2.seq1Range.start)) {
next = sequenceDiffs1.shift()!;
} else {
next = sequenceDiffs2.shift()!;
}
if (result.length > 0 && result[result.length - 1].seq1Range.endExclusive >= next.seq1Range.start) {
result[result.length - 1] = result[result.length - 1].join(next);
} else {
result.push(next);
}
}
return result;
}
export function removeVeryShortMatchingLinesBetweenDiffs(sequence1: LineSequence, _sequence2: LineSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
let diffs = sequenceDiffs;
if (diffs.length === 0) {
return diffs;
}
let counter = 0;
let shouldRepeat: boolean;
do {
shouldRepeat = false;
const result: SequenceDiff[] = [
diffs[0]
];
for (let i = 1; i < diffs.length; i++) {
const cur = diffs[i];
const lastResult = result[result.length - 1];
function shouldJoinDiffs(before: SequenceDiff, after: SequenceDiff): boolean {
const unchangedRange = new OffsetRange(lastResult.seq1Range.endExclusive, cur.seq1Range.start);
const unchangedText = sequence1.getText(unchangedRange);
const unchangedTextWithoutWs = unchangedText.replace(/\s/g, '');
if (unchangedTextWithoutWs.length <= 4
&& (before.seq1Range.length + before.seq2Range.length > 5 || after.seq1Range.length + after.seq2Range.length > 5)) {
return true;
}
return false;
}
const shouldJoin = shouldJoinDiffs(lastResult, cur);
if (shouldJoin) {
shouldRepeat = true;
result[result.length - 1] = result[result.length - 1].join(cur);
} else {
result.push(cur);
}
}
diffs = result;
} while (counter++ < 10 && shouldRepeat);
return diffs;
}
export function removeVeryShortMatchingTextBetweenLongDiffs(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] {
let diffs = sequenceDiffs;
if (diffs.length === 0) {
return diffs;
}
let counter = 0;
let shouldRepeat: boolean;
do {
shouldRepeat = false;
const result: SequenceDiff[] = [
diffs[0]
];
for (let i = 1; i < diffs.length; i++) {
const cur = diffs[i];
const lastResult = result[result.length - 1];
function shouldJoinDiffs(before: SequenceDiff, after: SequenceDiff): boolean {
const unchangedRange = new OffsetRange(lastResult.seq1Range.endExclusive, cur.seq1Range.start);
const unchangedLineCount = sequence1.countLinesIn(unchangedRange);
if (unchangedLineCount > 5 || unchangedRange.length > 500) {
return false;
}
const unchangedText = sequence1.getText(unchangedRange).trim();
if (unchangedText.length > 20 || unchangedText.split(/\r\n|\r|\n/).length > 1) {
return false;
}
const beforeLineCount1 = sequence1.countLinesIn(before.seq1Range);
const beforeSeq1Length = before.seq1Range.length;
const beforeLineCount2 = sequence2.countLinesIn(before.seq2Range);
const beforeSeq2Length = before.seq2Range.length;
const afterLineCount1 = sequence1.countLinesIn(after.seq1Range);
const afterSeq1Length = after.seq1Range.length;
const afterLineCount2 = sequence2.countLinesIn(after.seq2Range);
const afterSeq2Length = after.seq2Range.length;
// TODO: Maybe a neural net can be used to derive the result from these numbers
const max = 2 * 40 + 50;
function cap(v: number): number {
return Math.min(v, max);
}
if (Math.pow(Math.pow(cap(beforeLineCount1 * 40 + beforeSeq1Length), 1.5) + Math.pow(cap(beforeLineCount2 * 40 + beforeSeq2Length), 1.5), 1.5)
+ Math.pow(Math.pow(cap(afterLineCount1 * 40 + afterSeq1Length), 1.5) + Math.pow(cap(afterLineCount2 * 40 + afterSeq2Length), 1.5), 1.5) > ((max ** 1.5) ** 1.5) * 1.3) {
return true;
}
return false;
}
const shouldJoin = shouldJoinDiffs(lastResult, cur);
if (shouldJoin) {
shouldRepeat = true;
result[result.length - 1] = result[result.length - 1].join(cur);
} else {
result.push(cur);
}
}
diffs = result;
} while (counter++ < 10 && shouldRepeat);
// Remove short suffixes/prefixes
for (let i = 0; i < diffs.length; i++) {
const cur = diffs[i];
let range1 = cur.seq1Range;
let range2 = cur.seq2Range;
const fullRange1 = sequence1.extendToFullLines(cur.seq1Range);
const prefix = sequence1.getText(new OffsetRange(fullRange1.start, cur.seq1Range.start));
if (prefix.length > 0 && prefix.trim().length <= 3 && cur.seq1Range.length + cur.seq2Range.length > 100) {
range1 = cur.seq1Range.deltaStart(-prefix.length);
range2 = cur.seq2Range.deltaStart(-prefix.length);
}
const suffix = sequence1.getText(new OffsetRange(cur.seq1Range.endExclusive, fullRange1.endExclusive));
if (suffix.length > 0 && (suffix.trim().length <= 3 && cur.seq1Range.length + cur.seq2Range.length > 150)) {
range1 = range1.deltaEnd(suffix.length);
range2 = range2.deltaEnd(suffix.length);
}
diffs[i] = new SequenceDiff(range1, range2);
}
return diffs;
}
@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CharCode } from 'vs/base/common/charCode';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { ISequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm';
export class LineSequence implements ISequence {
constructor(
private readonly trimmedHash: number[],
private readonly lines: string[]
) { }
getElement(offset: number): number {
return this.trimmedHash[offset];
}
get length(): number {
return this.trimmedHash.length;
}
getBoundaryScore(length: number): number {
const indentationBefore = length === 0 ? 0 : getIndentation(this.lines[length - 1]);
const indentationAfter = length === this.lines.length ? 0 : getIndentation(this.lines[length]);
return 1000 - (indentationBefore + indentationAfter);
}
getText(range: OffsetRange): string {
return this.lines.slice(range.start, range.endExclusive).join('\n');
}
isStronglyEqual(offset1: number, offset2: number): boolean {
return this.lines[offset1] === this.lines[offset2];
}
}
function getIndentation(str: string): number {
let i = 0;
while (i < str.length && (str.charCodeAt(i) === CharCode.Space || str.charCodeAt(i) === CharCode.Tab)) {
i++;
}
return i;
}
@@ -0,0 +1,217 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { findLastIdxMonotonous, findLastMonotonous, findFirstMonotonous } from 'vs/base/common/arraysFind';
import { CharCode } from 'vs/base/common/charCode';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { ISequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm';
import { isSpace } from 'vs/editor/common/diff/defaultLinesDiffComputer/utils';
export class LinesSliceCharSequence implements ISequence {
private readonly elements: number[] = [];
private readonly firstCharOffsetByLine: number[] = [];
public readonly lineRange: OffsetRange;
// To account for trimming
private readonly additionalOffsetByLine: number[] = [];
constructor(public readonly lines: string[], lineRange: OffsetRange, public readonly considerWhitespaceChanges: boolean) {
// This slice has to have lineRange.length many \n! (otherwise diffing against an empty slice will be problematic)
// (Unless it covers the entire document, in that case the other slice also has to cover the entire document ands it's okay)
// If the slice covers the end, but does not start at the beginning, we include just the \n of the previous line.
let trimFirstLineFully = false;
if (lineRange.start > 0 && lineRange.endExclusive >= lines.length) {
lineRange = new OffsetRange(lineRange.start - 1, lineRange.endExclusive);
trimFirstLineFully = true;
}
this.lineRange = lineRange;
this.firstCharOffsetByLine[0] = 0;
for (let i = this.lineRange.start; i < this.lineRange.endExclusive; i++) {
let line = lines[i];
let offset = 0;
if (trimFirstLineFully) {
offset = line.length;
line = '';
trimFirstLineFully = false;
} else if (!considerWhitespaceChanges) {
const trimmedStartLine = line.trimStart();
offset = line.length - trimmedStartLine.length;
line = trimmedStartLine.trimEnd();
}
this.additionalOffsetByLine.push(offset);
for (let i = 0; i < line.length; i++) {
this.elements.push(line.charCodeAt(i));
}
// Don't add an \n that does not exist in the document.
if (i < lines.length - 1) {
this.elements.push('\n'.charCodeAt(0));
this.firstCharOffsetByLine[i - this.lineRange.start + 1] = this.elements.length;
}
}
// To account for the last line
this.additionalOffsetByLine.push(0);
}
toString() {
return `Slice: "${this.text}"`;
}
get text(): string {
return this.getText(new OffsetRange(0, this.length));
}
getText(range: OffsetRange): string {
return this.elements.slice(range.start, range.endExclusive).map(e => String.fromCharCode(e)).join('');
}
getElement(offset: number): number {
return this.elements[offset];
}
get length(): number {
return this.elements.length;
}
public getBoundaryScore(length: number): number {
// a b c , d e f
// 11 0 0 12 15 6 13 0 0 11
const prevCategory = getCategory(length > 0 ? this.elements[length - 1] : -1);
const nextCategory = getCategory(length < this.elements.length ? this.elements[length] : -1);
if (prevCategory === CharBoundaryCategory.LineBreakCR && nextCategory === CharBoundaryCategory.LineBreakLF) {
// don't break between \r and \n
return 0;
}
let score = 0;
if (prevCategory !== nextCategory) {
score += 10;
if (nextCategory === CharBoundaryCategory.WordUpper) {
score += 1;
}
}
score += getCategoryBoundaryScore(prevCategory);
score += getCategoryBoundaryScore(nextCategory);
return score;
}
public translateOffset(offset: number): Position {
// find smallest i, so that lineBreakOffsets[i] <= offset using binary search
if (this.lineRange.isEmpty) {
return new Position(this.lineRange.start + 1, 1);
}
const i = findLastIdxMonotonous(this.firstCharOffsetByLine, (value) => value <= offset);
return new Position(this.lineRange.start + i + 1, offset - this.firstCharOffsetByLine[i] + this.additionalOffsetByLine[i] + 1);
}
public translateRange(range: OffsetRange): Range {
return Range.fromPositions(this.translateOffset(range.start), this.translateOffset(range.endExclusive));
}
/**
* Finds the word that contains the character at the given offset
*/
public findWordContaining(offset: number): OffsetRange | undefined {
if (offset < 0 || offset >= this.elements.length) {
return undefined;
}
if (!isWordChar(this.elements[offset])) {
return undefined;
}
// find start
let start = offset;
while (start > 0 && isWordChar(this.elements[start - 1])) {
start--;
}
// find end
let end = offset;
while (end < this.elements.length && isWordChar(this.elements[end])) {
end++;
}
return new OffsetRange(start, end);
}
public countLinesIn(range: OffsetRange): number {
return this.translateOffset(range.endExclusive).lineNumber - this.translateOffset(range.start).lineNumber;
}
public isStronglyEqual(offset1: number, offset2: number): boolean {
return this.elements[offset1] === this.elements[offset2];
}
public extendToFullLines(range: OffsetRange): OffsetRange {
const start = findLastMonotonous(this.firstCharOffsetByLine, x => x <= range.start) ?? 0;
const end = findFirstMonotonous(this.firstCharOffsetByLine, x => range.endExclusive <= x) ?? this.elements.length;
return new OffsetRange(start, end);
}
}
function isWordChar(charCode: number): boolean {
return charCode >= CharCode.a && charCode <= CharCode.z
|| charCode >= CharCode.A && charCode <= CharCode.Z
|| charCode >= CharCode.Digit0 && charCode <= CharCode.Digit9;
}
const enum CharBoundaryCategory {
WordLower,
WordUpper,
WordNumber,
End,
Other,
Space,
LineBreakCR,
LineBreakLF,
}
const score: Record<CharBoundaryCategory, number> = {
[CharBoundaryCategory.WordLower]: 0,
[CharBoundaryCategory.WordUpper]: 0,
[CharBoundaryCategory.WordNumber]: 0,
[CharBoundaryCategory.End]: 10,
[CharBoundaryCategory.Other]: 2,
[CharBoundaryCategory.Space]: 3,
[CharBoundaryCategory.LineBreakCR]: 10,
[CharBoundaryCategory.LineBreakLF]: 10,
};
function getCategoryBoundaryScore(category: CharBoundaryCategory): number {
return score[category];
}
function getCategory(charCode: number): CharBoundaryCategory {
if (charCode === CharCode.LineFeed) {
return CharBoundaryCategory.LineBreakLF;
} else if (charCode === CharCode.CarriageReturn) {
return CharBoundaryCategory.LineBreakCR;
} else if (isSpace(charCode)) {
return CharBoundaryCategory.Space;
} else if (charCode >= CharCode.a && charCode <= CharCode.z) {
return CharBoundaryCategory.WordLower;
} else if (charCode >= CharCode.A && charCode <= CharCode.Z) {
return CharBoundaryCategory.WordUpper;
} else if (charCode >= CharCode.Digit0 && charCode <= CharCode.Digit9) {
return CharBoundaryCategory.WordNumber;
} else if (charCode === -1) {
return CharBoundaryCategory.End;
} else {
return CharBoundaryCategory.Other;
}
}
@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CharCode } from 'vs/base/common/charCode';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
export class Array2D<T> {
private readonly array: T[] = [];
constructor(public readonly width: number, public readonly height: number) {
this.array = new Array<T>(width * height);
}
get(x: number, y: number): T {
return this.array[x + y * this.width];
}
set(x: number, y: number, value: T): void {
this.array[x + y * this.width] = value;
}
}
export function isSpace(charCode: number): boolean {
return charCode === CharCode.Space || charCode === CharCode.Tab;
}
export class LineRangeFragment {
private static chrKeys = new Map<string, number>();
private static getKey(chr: string): number {
let key = this.chrKeys.get(chr);
if (key === undefined) {
key = this.chrKeys.size;
this.chrKeys.set(chr, key);
}
return key;
}
private readonly totalCount: number;
private readonly histogram: number[] = [];
constructor(
public readonly range: LineRange,
public readonly lines: string[],
public readonly source: DetailedLineRangeMapping,
) {
let counter = 0;
for (let i = range.startLineNumber - 1; i < range.endLineNumberExclusive - 1; i++) {
const line = lines[i];
for (let j = 0; j < line.length; j++) {
counter++;
const chr = line[j];
const key = LineRangeFragment.getKey(chr);
this.histogram[key] = (this.histogram[key] || 0) + 1;
}
counter++;
const key = LineRangeFragment.getKey('\n');
this.histogram[key] = (this.histogram[key] || 0) + 1;
}
this.totalCount = counter;
}
public computeSimilarity(other: LineRangeFragment): number {
let sumDifferences = 0;
const maxLength = Math.max(this.histogram.length, other.histogram.length);
for (let i = 0; i < maxLength; i++) {
sumDifferences += Math.abs((this.histogram[i] ?? 0) - (other.histogram[i] ?? 0));
}
return 1 - (sumDifferences / (this.totalCount + other.totalCount));
}
}
@@ -5,11 +5,13 @@
import { CancellationToken } from 'vs/base/common/cancellation';
import { Event } from 'vs/base/common/event';
import { LineRangeMapping, MovedText } from 'vs/editor/common/diff/linesDiffComputer';
import { MovedText } from 'vs/editor/common/diff/linesDiffComputer';
import { DetailedLineRangeMapping } from './rangeMapping';
import { ITextModel } from 'vs/editor/common/model';
/**
* A document diff provider computes the diff between two text models.
* @internal
*/
export interface IDocumentDiffProvider {
/**
@@ -26,6 +28,7 @@ export interface IDocumentDiffProvider {
/**
* Options for the diff computation.
* @internal
*/
export interface IDocumentDiffProviderOptions {
/**
@@ -46,6 +49,7 @@ export interface IDocumentDiffProviderOptions {
/**
* Represents a diff between two text models.
* @internal
*/
export interface IDocumentDiff {
/**
@@ -61,7 +65,7 @@ export interface IDocumentDiff {
/**
* Maps all modified line ranges in the original to the corresponding line ranges in the modified text model.
*/
readonly changes: readonly LineRangeMapping[];
readonly changes: readonly DetailedLineRangeMapping[];
/**
* Sorted by original line ranges.
@@ -5,7 +5,8 @@
import { CharCode } from 'vs/base/common/charCode';
import { IDiffChange, ISequence, LcsDiff, IDiffResult } from 'vs/base/common/diff/diff';
import { ILinesDiffComputer, ILinesDiffComputerOptions, RangeMapping, LineRangeMapping, LinesDiff } from 'vs/editor/common/diff/linesDiffComputer';
import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff } from 'vs/editor/common/diff/linesDiffComputer';
import { RangeMapping, DetailedLineRangeMapping } from './rangeMapping';
import * as strings from 'vs/base/common/strings';
import { Range } from 'vs/editor/common/core/range';
import { assertFn, checkAdjacentItems } from 'vs/base/common/assert';
@@ -23,8 +24,8 @@ export class LegacyLinesDiffComputer implements ILinesDiffComputer {
shouldPostProcessCharChanges: true,
});
const result = diffComputer.computeDiff();
const changes: LineRangeMapping[] = [];
let lastChange: LineRangeMapping | null = null;
const changes: DetailedLineRangeMapping[] = [];
let lastChange: DetailedLineRangeMapping | null = null;
for (const c of result.changes) {
@@ -44,17 +45,17 @@ export class LegacyLinesDiffComputer implements ILinesDiffComputer {
modifiedRange = new LineRange(c.modifiedStartLineNumber, c.modifiedEndLineNumber + 1);
}
let change = new LineRangeMapping(originalRange, modifiedRange, c.charChanges?.map(c => new RangeMapping(
let change = new DetailedLineRangeMapping(originalRange, modifiedRange, c.charChanges?.map(c => new RangeMapping(
new Range(c.originalStartLineNumber, c.originalStartColumn, c.originalEndLineNumber, c.originalEndColumn),
new Range(c.modifiedStartLineNumber, c.modifiedStartColumn, c.modifiedEndLineNumber, c.modifiedEndColumn),
)));
if (lastChange) {
if (lastChange.modifiedRange.endLineNumberExclusive === change.modifiedRange.startLineNumber
|| lastChange.originalRange.endLineNumberExclusive === change.originalRange.startLineNumber) {
if (lastChange.modified.endLineNumberExclusive === change.modified.startLineNumber
|| lastChange.original.endLineNumberExclusive === change.original.startLineNumber) {
// join touching diffs. Probably moving diffs up/down in the algorithm causes touching diffs.
change = new LineRangeMapping(
lastChange.originalRange.join(change.originalRange),
lastChange.modifiedRange.join(change.modifiedRange),
change = new DetailedLineRangeMapping(
lastChange.original.join(change.original),
lastChange.modified.join(change.modified),
lastChange.innerChanges && change.innerChanges ?
lastChange.innerChanges.concat(change.innerChanges) : undefined
);
@@ -68,10 +69,10 @@ export class LegacyLinesDiffComputer implements ILinesDiffComputer {
assertFn(() => {
return checkAdjacentItems(changes,
(m1, m2) => m2.originalRange.startLineNumber - m1.originalRange.endLineNumberExclusive === m2.modifiedRange.startLineNumber - m1.modifiedRange.endLineNumberExclusive &&
(m1, m2) => m2.original.startLineNumber - m1.original.endLineNumberExclusive === m2.modified.startLineNumber - m1.modified.endLineNumberExclusive &&
// There has to be an unchanged line in between (otherwise both diffs should have been joined)
m1.originalRange.endLineNumberExclusive < m2.originalRange.startLineNumber &&
m1.modifiedRange.endLineNumberExclusive < m2.modifiedRange.startLineNumber,
m1.original.endLineNumberExclusive < m2.original.startLineNumber &&
m1.modified.endLineNumberExclusive < m2.modified.startLineNumber,
);
});
@@ -92,7 +93,7 @@ export interface IDiffComputationResult {
/**
* The changes as (modern) line range mapping array.
*/
changes2: readonly LineRangeMapping[];
changes2: readonly DetailedLineRangeMapping[];
}
/**
+6 -136
View File
@@ -3,8 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Range } from 'vs/editor/common/core/range';
import { DetailedLineRangeMapping, LineRangeMapping } from './rangeMapping';
export interface ILinesDiffComputer {
computeDiff(originalLines: string[], modifiedLines: string[], options: ILinesDiffComputerOptions): LinesDiff;
@@ -18,7 +17,7 @@ export interface ILinesDiffComputerOptions {
export class LinesDiff {
constructor(
readonly changes: readonly LineRangeMapping[],
readonly changes: readonly DetailedLineRangeMapping[],
/**
* Sorted by original line ranges.
@@ -35,148 +34,19 @@ export class LinesDiff {
}
}
/**
* Maps a line range in the original text model to a line range in the modified text model.
*/
export class LineRangeMapping {
public static inverse(mapping: readonly LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): LineRangeMapping[] {
const result: LineRangeMapping[] = [];
let lastOriginalEndLineNumber = 1;
let lastModifiedEndLineNumber = 1;
for (const m of mapping) {
const r = new LineRangeMapping(
new LineRange(lastOriginalEndLineNumber, m.originalRange.startLineNumber),
new LineRange(lastModifiedEndLineNumber, m.modifiedRange.startLineNumber),
undefined
);
if (!r.modifiedRange.isEmpty) {
result.push(r);
}
lastOriginalEndLineNumber = m.originalRange.endLineNumberExclusive;
lastModifiedEndLineNumber = m.modifiedRange.endLineNumberExclusive;
}
const r = new LineRangeMapping(
new LineRange(lastOriginalEndLineNumber, originalLineCount + 1),
new LineRange(lastModifiedEndLineNumber, modifiedLineCount + 1),
undefined
);
if (!r.modifiedRange.isEmpty) {
result.push(r);
}
return result;
}
/**
* The line range in the original text model.
*/
public readonly originalRange: LineRange;
/**
* The line range in the modified text model.
*/
public readonly modifiedRange: LineRange;
/**
* If inner changes have not been computed, this is set to undefined.
* Otherwise, it represents the character-level diff in this line range.
* The original range of each range mapping should be contained in the original line range (same for modified), exceptions are new-lines.
* Must not be an empty array.
*/
public readonly innerChanges: RangeMapping[] | undefined;
constructor(
originalRange: LineRange,
modifiedRange: LineRange,
innerChanges: RangeMapping[] | undefined,
) {
this.originalRange = originalRange;
this.modifiedRange = modifiedRange;
this.innerChanges = innerChanges;
}
public toString(): string {
return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`;
}
public get changedLineCount() {
return Math.max(this.originalRange.length, this.modifiedRange.length);
}
public flip(): LineRangeMapping {
return new LineRangeMapping(this.modifiedRange, this.originalRange, this.innerChanges?.map(c => c.flip()));
}
}
/**
* Maps a range in the original text model to a range in the modified text model.
*/
export class RangeMapping {
/**
* The original range.
*/
readonly originalRange: Range;
/**
* The modified range.
*/
readonly modifiedRange: Range;
constructor(
originalRange: Range,
modifiedRange: Range,
) {
this.originalRange = originalRange;
this.modifiedRange = modifiedRange;
}
public toString(): string {
return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`;
}
public flip(): RangeMapping {
return new RangeMapping(this.modifiedRange, this.originalRange);
}
}
// TODO@hediet: Make LineRangeMapping extend from this!
export class SimpleLineRangeMapping {
constructor(
public readonly original: LineRange,
public readonly modified: LineRange,
) {
}
public toString(): string {
return `{${this.original.toString()}->${this.modified.toString()}}`;
}
public flip(): SimpleLineRangeMapping {
return new SimpleLineRangeMapping(this.modified, this.original);
}
public join(other: SimpleLineRangeMapping): SimpleLineRangeMapping {
return new SimpleLineRangeMapping(
this.original.join(other.original),
this.modified.join(other.modified),
);
}
}
export class MovedText {
public readonly lineRangeMapping: SimpleLineRangeMapping;
public readonly lineRangeMapping: LineRangeMapping;
/**
* The diff from the original text to the moved text.
* Must be contained in the original/modified line range.
* Can be empty if the text didn't change (only moved).
*/
public readonly changes: readonly LineRangeMapping[];
public readonly changes: readonly DetailedLineRangeMapping[];
constructor(
lineRangeMapping: SimpleLineRangeMapping,
changes: readonly LineRangeMapping[],
lineRangeMapping: LineRangeMapping,
changes: readonly DetailedLineRangeMapping[],
) {
this.lineRangeMapping = lineRangeMapping;
this.changes = changes;
@@ -4,9 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { LegacyLinesDiffComputer } from 'vs/editor/common/diff/legacyLinesDiffComputer';
import { AdvancedLinesDiffComputer } from 'vs/editor/common/diff/advancedLinesDiffComputer';
import { DefaultLinesDiffComputer } from 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer';
import { ILinesDiffComputer } from 'vs/editor/common/diff/linesDiffComputer';
export const linesDiffComputers = {
getLegacy: () => new LegacyLinesDiffComputer(),
getAdvanced: () => new AdvancedLinesDiffComputer(),
};
getDefault: () => new DefaultLinesDiffComputer(),
} satisfies Record<string, () => ILinesDiffComputer>;
+136
View File
@@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Range } from 'vs/editor/common/core/range';
/**
* Maps a line range in the original text model to a line range in the modified text model.
*/
export class LineRangeMapping {
public static inverse(mapping: readonly DetailedLineRangeMapping[], originalLineCount: number, modifiedLineCount: number): DetailedLineRangeMapping[] {
const result: DetailedLineRangeMapping[] = [];
let lastOriginalEndLineNumber = 1;
let lastModifiedEndLineNumber = 1;
for (const m of mapping) {
const r = new DetailedLineRangeMapping(
new LineRange(lastOriginalEndLineNumber, m.original.startLineNumber),
new LineRange(lastModifiedEndLineNumber, m.modified.startLineNumber),
undefined
);
if (!r.modified.isEmpty) {
result.push(r);
}
lastOriginalEndLineNumber = m.original.endLineNumberExclusive;
lastModifiedEndLineNumber = m.modified.endLineNumberExclusive;
}
const r = new DetailedLineRangeMapping(
new LineRange(lastOriginalEndLineNumber, originalLineCount + 1),
new LineRange(lastModifiedEndLineNumber, modifiedLineCount + 1),
undefined
);
if (!r.modified.isEmpty) {
result.push(r);
}
return result;
}
/**
* The line range in the original text model.
*/
public readonly original: LineRange;
/**
* The line range in the modified text model.
*/
public readonly modified: LineRange;
constructor(
originalRange: LineRange,
modifiedRange: LineRange
) {
this.original = originalRange;
this.modified = modifiedRange;
}
public toString(): string {
return `{${this.original.toString()}->${this.modified.toString()}}`;
}
public flip(): LineRangeMapping {
return new LineRangeMapping(this.modified, this.original);
}
public join(other: LineRangeMapping): LineRangeMapping {
return new LineRangeMapping(
this.original.join(other.original),
this.modified.join(other.modified)
);
}
public get changedLineCount() {
return Math.max(this.original.length, this.modified.length);
}
}
/**
* Maps a line range in the original text model to a line range in the modified text model.
* Also contains inner range mappings.
*/
export class DetailedLineRangeMapping extends LineRangeMapping {
/**
* If inner changes have not been computed, this is set to undefined.
* Otherwise, it represents the character-level diff in this line range.
* The original range of each range mapping should be contained in the original line range (same for modified), exceptions are new-lines.
* Must not be an empty array.
*/
public readonly innerChanges: RangeMapping[] | undefined;
constructor(
originalRange: LineRange,
modifiedRange: LineRange,
innerChanges: RangeMapping[] | undefined
) {
super(originalRange, modifiedRange);
this.innerChanges = innerChanges;
}
public override flip(): DetailedLineRangeMapping {
return new DetailedLineRangeMapping(this.modified, this.original, this.innerChanges?.map(c => c.flip()));
}
}
/**
* Maps a range in the original text model to a range in the modified text model.
*/
export class RangeMapping {
/**
* The original range.
*/
readonly originalRange: Range;
/**
* The modified range.
*/
readonly modifiedRange: Range;
constructor(
originalRange: Range,
modifiedRange: Range
) {
this.originalRange = originalRange;
this.modifiedRange = modifiedRange;
}
public toString(): string {
return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`;
}
public flip(): RangeMapping {
return new RangeMapping(this.modifiedRange, this.originalRange);
}
}
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { findLast } from 'vs/base/common/arrays';
import { findLast } from 'vs/base/common/arraysFind';
import * as strings from 'vs/base/common/strings';
import { CursorColumns } from 'vs/editor/common/core/cursorColumns';
import { IPosition, Position } from 'vs/editor/common/core/position';

Some files were not shown because too many files have changed in this diff Show More