mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-28 05:37:48 +01:00
Merge branch 'main' into aamunger/notebookSymbols
This commit is contained in:
@@ -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"
|
||||
|
||||
Vendored
+5
-1
@@ -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",
|
||||
|
||||
Vendored
-1
@@ -151,5 +151,4 @@
|
||||
"application.experimental.rendererProfiling": true,
|
||||
"editor.experimental.asyncTokenization": true,
|
||||
"editor.experimental.asyncTokenizationVerification": true,
|
||||
"diffEditor.experimental.useVersion2": true,
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
Vendored
+2
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Vendored
+22
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ export {
|
||||
debouncedObservable,
|
||||
derivedObservableWithCache,
|
||||
derivedObservableWithWritableCache,
|
||||
keepAlive,
|
||||
keepObserved,
|
||||
recomputeInitiallyAndOnChange,
|
||||
observableFromEvent,
|
||||
observableFromPromise,
|
||||
observableSignal,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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)),
|
||||
|
||||
|
||||
@@ -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);
|
||||
+38
-30
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+19
-1
@@ -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);
|
||||
}
|
||||
|
||||
+2
-2
@@ -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.
|
||||
+1
-1
@@ -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]);
|
||||
}
|
||||
+262
-167
@@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user