diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 30b783943e03816b683e7e8a43ef743db9cc0c8a..f68880d6f68a44b9ba52960d234a9bdcd1388172 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,7 +3,7 @@ "tasks": [ { "type": "npm", - "script": "watch", + "script": "watchd", "label": "Build VS Code", "group": { "kind": "build", @@ -31,6 +31,16 @@ } } }, + { + "type": "npm", + "script": "kill-watchd", + "label": "Kill Build VS Code", + "group": "build", + "presentation": { + "reveal": "never" + }, + "problemMatcher": "$tsc" + }, { "label": "Run tests", "type": "shell", diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 8ae6053734e88f3f0b3d8f329a25bf66511ff6c9..f3de788998f094b40883fd37e0915951b875f250 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -435,14 +435,13 @@ export class Git { const [, letter] = match; try { - let networkPath = await new Promise(resolve => + const networkPath = await new Promise(resolve => realpath.native(`${letter}:`, { encoding: 'utf8' }, (err, resolvedPath) => // eslint-disable-next-line eqeqeq resolve(err != null ? undefined : resolvedPath), ), ); if (networkPath !== undefined) { - networkPath = `${networkPath}\\`; return path.normalize( repoUri.fsPath.replace( networkPath, diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/features/completions.ts index 4d7f2cf1626b363375440ba099306b327513d838..e1ef02c998c248efd9c5e6e4f67a5f4831185b9d 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/features/completions.ts @@ -13,7 +13,6 @@ import { nulToken } from '../utils/cancellation'; import { applyCodeAction } from '../utils/codeAction'; import { Command, CommandManager } from '../utils/commandManager'; import { ConfigurationDependentRegistration } from '../utils/dependentRegistration'; -import { memoize } from '../utils/memoize'; import * as Previewer from '../utils/previewer'; import { snippetForFunctionCall } from '../utils/snippetForFunctionCall'; import { TelemetryReporter } from '../utils/telemetry'; @@ -68,7 +67,9 @@ class MyCompletionItem extends vscode.CompletionItem { this.preselect = tsEntry.isRecommended; this.position = position; this.useCodeSnippet = completionContext.useCodeSnippetsOnMethodSuggest && (this.kind === vscode.CompletionItemKind.Function || this.kind === vscode.CompletionItemKind.Method); + this.range = this.getRangeFromReplacementSpan(tsEntry, completionContext, position); + this.commitCharacters = MyCompletionItem.getCommitCharacters(completionContext, tsEntry); this.insertText = tsEntry.insertText; this.filterText = this.getFilterText(completionContext.line, tsEntry.insertText); @@ -268,14 +269,13 @@ class MyCompletionItem extends vscode.CompletionItem { } } - @memoize - public get commitCharacters(): string[] | undefined { - if (this.completionContext.isNewIdentifierLocation || !this.completionContext.isInValidCommitCharacterContext) { + private static getCommitCharacters(context: CompletionContext, entry: Proto.CompletionEntry): string[] | undefined { + if (context.isNewIdentifierLocation || !context.isInValidCommitCharacterContext) { return undefined; } const commitCharacters: string[] = []; - switch (this.tsEntry.kind) { + switch (entry.kind) { case PConst.Kind.memberGetAccessor: case PConst.Kind.memberSetAccessor: case PConst.Kind.constructSignature: @@ -299,7 +299,7 @@ class MyCompletionItem extends vscode.CompletionItem { case PConst.Kind.keyword: case PConst.Kind.parameter: commitCharacters.push('.', ',', ';'); - if (this.completionContext.enableCallCompletions) { + if (context.enableCallCompletions) { commitCharacters.push('('); } break; diff --git a/extensions/typescript-language-features/src/features/rename.ts b/extensions/typescript-language-features/src/features/rename.ts index db68acf38f3a239ad6cba8a298d9bcb7a6f606e8..9074faa43729a7554edb2e8293ee9d6775be8370 100644 --- a/extensions/typescript-language-features/src/features/rename.ts +++ b/extensions/typescript-language-features/src/features/rename.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; +import API from '../utils/api'; import * as typeConverters from '../utils/typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; @@ -24,6 +25,10 @@ class TypeScriptRenameProvider implements vscode.RenameProvider { position: vscode.Position, token: vscode.CancellationToken ): Promise { + if (this.client.apiVersion.lt(API.v310)) { + return null; + } + const response = await this.execRename(document, position, token); if (response?.type !== 'response' || !response.body) { return null; @@ -34,12 +39,7 @@ class TypeScriptRenameProvider implements vscode.RenameProvider { return Promise.reject(renameInfo.localizedErrorMessage); } - const triggerSpan = renameInfo.triggerSpan; // added in TS 3.1 - if (triggerSpan) { - return typeConverters.Range.fromTextSpan(triggerSpan); - } - - return null; + return typeConverters.Range.fromTextSpan(renameInfo.triggerSpan); } public async provideRenameEdits( diff --git a/package.json b/package.json index 07aef019358ecc68afe01d11b05c3014ba131f33..e642009dff20f37701a329c707185e250dfcd386 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "native-watchdog": "1.3.0", "node-pty": "0.10.0-beta8", "onigasm-umd": "2.2.5", - "semver-umd": "^5.5.5", + "semver-umd": "^5.5.6", "spdlog": "^0.11.1", "sudo-prompt": "9.1.1", "v8-inspect-profiler": "^0.0.20", diff --git a/remote/package.json b/remote/package.json index 6ed1fa0c3bd0582c3512ad12fa77db8fe75bc5bc..794ce654a2783e5c3e8f1e4fdc084ae0a9d07450 100644 --- a/remote/package.json +++ b/remote/package.json @@ -14,7 +14,7 @@ "native-watchdog": "1.3.0", "node-pty": "0.10.0-beta8", "onigasm-umd": "2.2.5", - "semver-umd": "^5.5.5", + "semver-umd": "^5.5.6", "spdlog": "^0.11.1", "vscode-nsfw": "1.2.8", "vscode-proxy-agent": "^0.5.2", diff --git a/remote/web/package.json b/remote/web/package.json index ba587deca03001a7afbeb7d319b5f6ecfde74849..65aee5c9b390a4a5486a39389f745999b2aca194 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "dependencies": { "onigasm-umd": "2.2.5", - "semver-umd": "^5.5.5", + "semver-umd": "^5.5.6", "vscode-textmate": "4.4.0", "xterm": "4.6.0-beta.4", "xterm-addon-search": "0.6.0", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index e1ffafbe05f7fd6be2dd637aed8b45efad786c93..24495cfcd4a98c99bb69e09ed53945c6aa046113 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -19,10 +19,10 @@ oniguruma@^7.2.0: dependencies: nan "^2.14.0" -semver-umd@^5.5.5: - version "5.5.5" - resolved "https://registry.yarnpkg.com/semver-umd/-/semver-umd-5.5.5.tgz#a2e4280d0e92a2b27695c18811f0e939e144d86f" - integrity sha512-8rUq0nnTzlexpAdYmm8UDYsLkBn0MnBkfrGWPmyDBDDzv71dPOH07szOOaLj/5hO3BYmumYwS+wp3C60zLzh5g== +semver-umd@^5.5.6: + version "5.5.6" + resolved "https://registry.yarnpkg.com/semver-umd/-/semver-umd-5.5.6.tgz#1d185bbd2caec825c564b54907cd09e14083f228" + integrity sha512-6ARYXVi4Y4VO5HfyCjT/6xyykBtJwEXSGQ8ON4UPQSFOjZUDsbAE0J614QcBBsLTTyQMEqvsXN804vAqpydjzw== vscode-textmate@4.4.0: version "4.4.0" diff --git a/remote/yarn.lock b/remote/yarn.lock index 4dbea32fc9df8be3d8898d8476e5c0a399352492..89fe959a97fe81e1c00e3defee69092ba37b54ae 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -317,10 +317,10 @@ readdirp@~3.2.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -semver-umd@^5.5.5: - version "5.5.5" - resolved "https://registry.yarnpkg.com/semver-umd/-/semver-umd-5.5.5.tgz#a2e4280d0e92a2b27695c18811f0e939e144d86f" - integrity sha512-8rUq0nnTzlexpAdYmm8UDYsLkBn0MnBkfrGWPmyDBDDzv71dPOH07szOOaLj/5hO3BYmumYwS+wp3C60zLzh5g== +semver-umd@^5.5.6: + version "5.5.6" + resolved "https://registry.yarnpkg.com/semver-umd/-/semver-umd-5.5.6.tgz#1d185bbd2caec825c564b54907cd09e14083f228" + integrity sha512-6ARYXVi4Y4VO5HfyCjT/6xyykBtJwEXSGQ8ON4UPQSFOjZUDsbAE0J614QcBBsLTTyQMEqvsXN804vAqpydjzw== semver@^5.3.0: version "5.6.0" diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index 3da8d52b3995139b9343d78971539f0a56af49d4..3f3f73dba4cc77d9cc191519031699a630e7702a 100644 --- a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css +++ b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css @@ -5,7 +5,7 @@ @font-face { font-family: "codicon"; - src: url("./codicon.ttf?c49b146a6d13d30d08fbc195f20565aa") format("truetype"); + src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype"); } .codicon[class*='codicon-'] { diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index 18cc62f911d0fc8c1b366772a76a3f11d7bf328c..ab67bd300fc0c13ca9847283c36157cab45d84f3 100644 Binary files a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf and b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf differ diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index e0528d52ea3285ef1d84dae6f23e527f4b76a927..43859c89cff760ffdcc8d060d393218e6887d03e 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -153,6 +153,8 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } private hide(): void { + this.renderDisposeables.clear(); + if (!this.visible) { return; } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index a664d7ec9afd41689a107fb74194b41ca705eb87..9793ff3d135ad0d162d9231baa6bcf5836e03f60 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -9803,6 +9803,11 @@ declare module 'vscode' { * A string to show as placeholder in the input box to guide the user. */ placeholder: string; + + /** + * Controls whether the input box is visible (default is `true`). + */ + visible: boolean; } interface QuickDiffProvider { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index a75cf81d5180b2e64f87ebfc25da5332f26d793d..402b886a0a81b2ffbccf95903512a2a9a985be4a 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -828,21 +828,6 @@ declare module 'vscode' { //#endregion - //#region Joao: SCM Input Box - - /** - * Represents the input box in the Source Control viewlet. - */ - export interface SourceControlInputBox { - - /** - * Controls whether the input box is visible (default is `true`). - */ - visible: boolean; - } - - //#endregion - //#region Terminal data write event https://github.com/microsoft/vscode/issues/78502 export interface TerminalDataWriteEvent { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 0f52d71429038e50d14962c83d64141d3c50e541..0584726e363563c1b68cdf6bbe0dec031911aeee 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -406,7 +406,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return { label: data[ISuggestDataDtoField.label2] || data[ISuggestDataDtoField.label], - kind: data[ISuggestDataDtoField.kind], + kind: data[ISuggestDataDtoField.kind] || modes.CompletionItemKind.Property, tags: data[ISuggestDataDtoField.kindModifier], detail: data[ISuggestDataDtoField.detail], documentation: data[ISuggestDataDtoField.documentation], diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 279cb6cbc13260f9fda2a909ba94a322ce7d60e2..bf71fbba84b4823a121a9c61a74c432e5c5978a4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1104,7 +1104,7 @@ export const enum ISuggestDataDtoField { export interface ISuggestDataDto { [ISuggestDataDtoField.label]: string; [ISuggestDataDtoField.label2]?: string | modes.CompletionItemLabel; - [ISuggestDataDtoField.kind]: modes.CompletionItemKind; + [ISuggestDataDtoField.kind]?: modes.CompletionItemKind; [ISuggestDataDtoField.detail]?: string; [ISuggestDataDtoField.documentation]?: string | IMarkdownString; [ISuggestDataDtoField.sortText]?: string; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 1f8d2bc15f04fdd0933df0803af7712a83142f1e..e97f9f1d862789452c967ec69fcb14f7caebb7a2 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -956,7 +956,7 @@ class SuggestAdapter { // [extHostProtocol.ISuggestDataDtoField.label]: item.label, [extHostProtocol.ISuggestDataDtoField.label2]: item.label2, - [extHostProtocol.ISuggestDataDtoField.kind]: typeConvert.CompletionItemKind.from(item.kind), + [extHostProtocol.ISuggestDataDtoField.kind]: item.kind ? typeConvert.CompletionItemKind.from(item.kind) : undefined, [extHostProtocol.ISuggestDataDtoField.kindModifier]: item.tags && item.tags.map(typeConvert.CompletionItemTag.from), [extHostProtocol.ISuggestDataDtoField.detail]: item.detail, [extHostProtocol.ISuggestDataDtoField.documentation]: typeof item.documentation === 'undefined' ? undefined : typeConvert.MarkdownString.fromStrict(item.documentation), diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index fb3f7725d3a4da5c487de2b8d8be40502cc3bf84..4d08d98756489c8ca412c44017b5a2c1425b1824 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -194,6 +194,11 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { set visible(visible: boolean) { visible = !!visible; + + if (this._visible === visible) { + return; + } + this._visible = visible; this._proxy.$setInputBoxVisibility(this._sourceControlHandle, visible); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 2093ebadf371f36e379c7c5cf3e8a525ae50b2dd..2aaa1d3e7e23fb1ccf0dd6e954662dcbc2085246 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -808,70 +808,72 @@ export namespace CompletionItemTag { export namespace CompletionItemKind { - export function from(kind: types.CompletionItemKind | undefined): modes.CompletionItemKind { - switch (kind) { - case types.CompletionItemKind.Method: return modes.CompletionItemKind.Method; - case types.CompletionItemKind.Function: return modes.CompletionItemKind.Function; - case types.CompletionItemKind.Constructor: return modes.CompletionItemKind.Constructor; - case types.CompletionItemKind.Field: return modes.CompletionItemKind.Field; - case types.CompletionItemKind.Variable: return modes.CompletionItemKind.Variable; - case types.CompletionItemKind.Class: return modes.CompletionItemKind.Class; - case types.CompletionItemKind.Interface: return modes.CompletionItemKind.Interface; - case types.CompletionItemKind.Struct: return modes.CompletionItemKind.Struct; - case types.CompletionItemKind.Module: return modes.CompletionItemKind.Module; - case types.CompletionItemKind.Property: return modes.CompletionItemKind.Property; - case types.CompletionItemKind.Unit: return modes.CompletionItemKind.Unit; - case types.CompletionItemKind.Value: return modes.CompletionItemKind.Value; - case types.CompletionItemKind.Constant: return modes.CompletionItemKind.Constant; - case types.CompletionItemKind.Enum: return modes.CompletionItemKind.Enum; - case types.CompletionItemKind.EnumMember: return modes.CompletionItemKind.EnumMember; - case types.CompletionItemKind.Keyword: return modes.CompletionItemKind.Keyword; - case types.CompletionItemKind.Snippet: return modes.CompletionItemKind.Snippet; - case types.CompletionItemKind.Text: return modes.CompletionItemKind.Text; - case types.CompletionItemKind.Color: return modes.CompletionItemKind.Color; - case types.CompletionItemKind.File: return modes.CompletionItemKind.File; - case types.CompletionItemKind.Reference: return modes.CompletionItemKind.Reference; - case types.CompletionItemKind.Folder: return modes.CompletionItemKind.Folder; - case types.CompletionItemKind.Event: return modes.CompletionItemKind.Event; - case types.CompletionItemKind.Operator: return modes.CompletionItemKind.Operator; - case types.CompletionItemKind.TypeParameter: return modes.CompletionItemKind.TypeParameter; - case types.CompletionItemKind.Issue: return modes.CompletionItemKind.Issue; - case types.CompletionItemKind.User: return modes.CompletionItemKind.User; - } - return modes.CompletionItemKind.Property; - } + const _from = new Map([ + [types.CompletionItemKind.Method, modes.CompletionItemKind.Method], + [types.CompletionItemKind.Function, modes.CompletionItemKind.Function], + [types.CompletionItemKind.Constructor, modes.CompletionItemKind.Constructor], + [types.CompletionItemKind.Field, modes.CompletionItemKind.Field], + [types.CompletionItemKind.Variable, modes.CompletionItemKind.Variable], + [types.CompletionItemKind.Class, modes.CompletionItemKind.Class], + [types.CompletionItemKind.Interface, modes.CompletionItemKind.Interface], + [types.CompletionItemKind.Struct, modes.CompletionItemKind.Struct], + [types.CompletionItemKind.Module, modes.CompletionItemKind.Module], + [types.CompletionItemKind.Property, modes.CompletionItemKind.Property], + [types.CompletionItemKind.Unit, modes.CompletionItemKind.Unit], + [types.CompletionItemKind.Value, modes.CompletionItemKind.Value], + [types.CompletionItemKind.Constant, modes.CompletionItemKind.Constant], + [types.CompletionItemKind.Enum, modes.CompletionItemKind.Enum], + [types.CompletionItemKind.EnumMember, modes.CompletionItemKind.EnumMember], + [types.CompletionItemKind.Keyword, modes.CompletionItemKind.Keyword], + [types.CompletionItemKind.Snippet, modes.CompletionItemKind.Snippet], + [types.CompletionItemKind.Text, modes.CompletionItemKind.Text], + [types.CompletionItemKind.Color, modes.CompletionItemKind.Color], + [types.CompletionItemKind.File, modes.CompletionItemKind.File], + [types.CompletionItemKind.Reference, modes.CompletionItemKind.Reference], + [types.CompletionItemKind.Folder, modes.CompletionItemKind.Folder], + [types.CompletionItemKind.Event, modes.CompletionItemKind.Event], + [types.CompletionItemKind.Operator, modes.CompletionItemKind.Operator], + [types.CompletionItemKind.TypeParameter, modes.CompletionItemKind.TypeParameter], + [types.CompletionItemKind.Issue, modes.CompletionItemKind.Issue], + [types.CompletionItemKind.User, modes.CompletionItemKind.User], + ]); + + export function from(kind: types.CompletionItemKind): modes.CompletionItemKind { + return _from.get(kind) ?? modes.CompletionItemKind.Property; + } + + const _to = new Map([ + [modes.CompletionItemKind.Method, types.CompletionItemKind.Method], + [modes.CompletionItemKind.Function, types.CompletionItemKind.Function], + [modes.CompletionItemKind.Constructor, types.CompletionItemKind.Constructor], + [modes.CompletionItemKind.Field, types.CompletionItemKind.Field], + [modes.CompletionItemKind.Variable, types.CompletionItemKind.Variable], + [modes.CompletionItemKind.Class, types.CompletionItemKind.Class], + [modes.CompletionItemKind.Interface, types.CompletionItemKind.Interface], + [modes.CompletionItemKind.Struct, types.CompletionItemKind.Struct], + [modes.CompletionItemKind.Module, types.CompletionItemKind.Module], + [modes.CompletionItemKind.Property, types.CompletionItemKind.Property], + [modes.CompletionItemKind.Unit, types.CompletionItemKind.Unit], + [modes.CompletionItemKind.Value, types.CompletionItemKind.Value], + [modes.CompletionItemKind.Constant, types.CompletionItemKind.Constant], + [modes.CompletionItemKind.Enum, types.CompletionItemKind.Enum], + [modes.CompletionItemKind.EnumMember, types.CompletionItemKind.EnumMember], + [modes.CompletionItemKind.Keyword, types.CompletionItemKind.Keyword], + [modes.CompletionItemKind.Snippet, types.CompletionItemKind.Snippet], + [modes.CompletionItemKind.Text, types.CompletionItemKind.Text], + [modes.CompletionItemKind.Color, types.CompletionItemKind.Color], + [modes.CompletionItemKind.File, types.CompletionItemKind.File], + [modes.CompletionItemKind.Reference, types.CompletionItemKind.Reference], + [modes.CompletionItemKind.Folder, types.CompletionItemKind.Folder], + [modes.CompletionItemKind.Event, types.CompletionItemKind.Event], + [modes.CompletionItemKind.Operator, types.CompletionItemKind.Operator], + [modes.CompletionItemKind.TypeParameter, types.CompletionItemKind.TypeParameter], + [modes.CompletionItemKind.User, types.CompletionItemKind.User], + [modes.CompletionItemKind.Issue, types.CompletionItemKind.Issue], + ]); export function to(kind: modes.CompletionItemKind): types.CompletionItemKind { - switch (kind) { - case modes.CompletionItemKind.Method: return types.CompletionItemKind.Method; - case modes.CompletionItemKind.Function: return types.CompletionItemKind.Function; - case modes.CompletionItemKind.Constructor: return types.CompletionItemKind.Constructor; - case modes.CompletionItemKind.Field: return types.CompletionItemKind.Field; - case modes.CompletionItemKind.Variable: return types.CompletionItemKind.Variable; - case modes.CompletionItemKind.Class: return types.CompletionItemKind.Class; - case modes.CompletionItemKind.Interface: return types.CompletionItemKind.Interface; - case modes.CompletionItemKind.Struct: return types.CompletionItemKind.Struct; - case modes.CompletionItemKind.Module: return types.CompletionItemKind.Module; - case modes.CompletionItemKind.Property: return types.CompletionItemKind.Property; - case modes.CompletionItemKind.Unit: return types.CompletionItemKind.Unit; - case modes.CompletionItemKind.Value: return types.CompletionItemKind.Value; - case modes.CompletionItemKind.Constant: return types.CompletionItemKind.Constant; - case modes.CompletionItemKind.Enum: return types.CompletionItemKind.Enum; - case modes.CompletionItemKind.EnumMember: return types.CompletionItemKind.EnumMember; - case modes.CompletionItemKind.Keyword: return types.CompletionItemKind.Keyword; - case modes.CompletionItemKind.Snippet: return types.CompletionItemKind.Snippet; - case modes.CompletionItemKind.Text: return types.CompletionItemKind.Text; - case modes.CompletionItemKind.Color: return types.CompletionItemKind.Color; - case modes.CompletionItemKind.File: return types.CompletionItemKind.File; - case modes.CompletionItemKind.Reference: return types.CompletionItemKind.Reference; - case modes.CompletionItemKind.Folder: return types.CompletionItemKind.Folder; - case modes.CompletionItemKind.Event: return types.CompletionItemKind.Event; - case modes.CompletionItemKind.Operator: return types.CompletionItemKind.Operator; - case modes.CompletionItemKind.TypeParameter: return types.CompletionItemKind.TypeParameter; - case modes.CompletionItemKind.User: return types.CompletionItemKind.User; - case modes.CompletionItemKind.Issue: return types.CompletionItemKind.Issue; - } - return types.CompletionItemKind.Property; + return _to.get(kind) ?? types.CompletionItemKind.Property; } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 6e18a38dfe44e7192a64e149c2f038c32381cceb..a86fe3980a03ddb2eef359669f8d87e3478010be 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -695,7 +695,7 @@ class StatusbarEntryItem extends Disposable { } if (!this.entry || entry.ariaLabel !== this.entry.ariaLabel) { - this.container.setAttribute('aria-label', entry.ariaLabel); + this.labelContainer.setAttribute('aria-label', entry.ariaLabel); } // Update: Tooltip (on the container, because label can be disabled) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts index dbfdec6c4a30120b4692f8f35be3cb434a68b74b..f13fa4996ac64b4c66cbc3b76a6bf7de0984b02b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts @@ -171,7 +171,7 @@ registerAction2(class extends Action2 { return; } - const idx = editor.viewModel?.getViewCellIndex(activeCell); + const idx = editor.viewModel?.getCellIndex(activeCell); if (typeof idx !== 'number') { return; } @@ -453,7 +453,7 @@ async function changeActiveCellToKind(kind: CellKind, accessor: ServicesAccessor const text = activeCell.getText(); await editor.insertNotebookCell(activeCell, kind, 'below', text); - const idx = editor.viewModel?.getViewCellIndex(activeCell); + const idx = editor.viewModel?.getCellIndex(activeCell); if (typeof idx !== 'number') { return; } @@ -522,7 +522,8 @@ registerAction2(class extends InsertCellCommand { super( { id: INSERT_CODE_CELL_ABOVE_COMMAND_ID, - title: localize('notebookActions.insertCodeCellAbove', "Insert Code Cell Above") + title: localize('notebookActions.insertCodeCellAbove', "Insert Code Cell Above"), + f1: true }, CellKind.Code, 'above'); @@ -578,6 +579,7 @@ registerAction2(class extends InsertCellCommand { { id: INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"), + f1: true }, CellKind.Markdown, 'above'); @@ -860,7 +862,7 @@ registerAction2(class extends Action2 { return; } - viewModel.deleteCell(viewModel.getViewCellIndex(context.cell), true); + viewModel.deleteCell(viewModel.getCellIndex(context.cell), true); notebookService.setToCopy([context.cell.model]); } }); @@ -898,7 +900,7 @@ registerAction2(class extends Action2 { return; } - const currCellIndex = viewModel.getViewCellIndex(context!.cell); + const currCellIndex = viewModel.getCellIndex(context!.cell); pasteCells.reverse().forEach(pasteCell => { viewModel.insertCell(currCellIndex, pasteCell, true); @@ -939,7 +941,7 @@ registerAction2(class extends Action2 { return; } - const currCellIndex = viewModel.getViewCellIndex(context!.cell); + const currCellIndex = viewModel.getCellIndex(context!.cell); pasteCells.reverse().forEach(pasteCell => { viewModel.insertCell(currCellIndex + 1, pasteCell, true); @@ -1018,7 +1020,7 @@ registerAction2(class extends Action2 { const editor = context.notebookEditor; const activeCell = context.cell; - const idx = editor.viewModel?.getViewCellIndex(activeCell); + const idx = editor.viewModel?.getCellIndex(activeCell); if (typeof idx !== 'number') { return; } @@ -1057,7 +1059,7 @@ registerAction2(class extends Action2 { const editor = context.notebookEditor; const activeCell = context.cell; - const idx = editor.viewModel?.getViewCellIndex(activeCell); + const idx = editor.viewModel?.getCellIndex(activeCell); if (typeof idx !== 'number') { return; } @@ -1174,3 +1176,96 @@ registerAction2(class extends Action2 { } } }); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.testSetHiddenRanges1', + title: 'Notebook Cells set hidden ranges: 2,3,4', + category: NOTEBOOK_ACTIONS_CATEGORY, + keybinding: { + when: IsDevelopmentContext, + primary: undefined, + weight: KeybindingWeight.WorkbenchContrib + }, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const resource = editorService.activeEditor?.resource; + if (!resource) { + return; + } + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + editor.setHiddenAreas([{ start: 1, length: 3 }]); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.testSetHiddenRanges2', + title: 'Notebook Cells set hidden ranges: 4,5', + category: NOTEBOOK_ACTIONS_CATEGORY, + keybinding: { + when: IsDevelopmentContext, + primary: undefined, + weight: KeybindingWeight.WorkbenchContrib + }, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const resource = editorService.activeEditor?.resource; + if (!resource) { + return; + } + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + editor.setHiddenAreas([{ start: 3, length: 2 }]); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.resetHiddenAreas', + title: 'Notebook Cells reset hidden ranges', + category: NOTEBOOK_ACTIONS_CATEGORY, + keybinding: { + when: IsDevelopmentContext, + primary: undefined, + weight: KeybindingWeight.WorkbenchContrib + }, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const resource = editorService.activeEditor?.resource; + if (!resource) { + return; + } + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + editor.setHiddenAreas([]); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index cf4e285480b089597e121d4fa59d5f77a6f0891b..547a5fe3efab537aac89c4e3ca711482c337055d 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -135,11 +135,11 @@ cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows .monaco-list-row { +.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { width: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows .monaco-list-row > .monaco-toolbar { +.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { visibility: hidden; display: inline-block; @@ -151,7 +151,7 @@ box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.2); } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows .monaco-list-row > .monaco-toolbar .action-item { +.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item { width: 24px; height: 24px; display: flex; @@ -159,7 +159,7 @@ margin: 1px 2px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows .monaco-list-row > .monaco-toolbar .action-item .action-label { +.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item .action-label { display: flex; align-items: center; margin: auto; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 8e7dd4a9c34eebfb2955f0f5aab89c728e6d53dd..7cf0976cf063f0278bc1120dcc1ffd5962a8b4a3 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from 'vs/base/common/event'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; @@ -20,6 +21,9 @@ import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } fro import { CellKind, IOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { ScrollEvent } from 'vs/base/common/scrollable'; +import { IListStyles, IListOptions } from 'vs/base/browser/ui/list/listWidget'; +import { IListEvent } from 'vs/base/browser/ui/list/list'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); @@ -249,6 +253,11 @@ export interface INotebookEditor { */ revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void; + /** + * Set hidden areas on cell text models. + */ + setHiddenAreas(_ranges: ICellRange[]): boolean; + setCellSelection(cell: ICellViewModel, selection: Range): void; /** @@ -270,6 +279,45 @@ export interface INotebookEditor { hideFind(): void; } +export interface INotebookCellList { + onWillScroll: Event; + onDidChangeFocus: Event>; + onDidChangeContentHeight: Event; + scrollTop: number; + scrollHeight: number; + scrollLeft: number; + length: number; + rowsContainer: HTMLElement; + readonly onDidRemoveOutput: Event; + detachViewModel(): void; + attachViewModel(viewModel: NotebookViewModel): void; + clear(): void; + focusElement(element: ICellViewModel): void; + selectElement(element: ICellViewModel): void; + getFocusedElements(): ICellViewModel[]; + revealElementInView(element: ICellViewModel): void; + revealElementInCenterIfOutsideViewport(element: ICellViewModel): void; + revealElementInCenter(element: ICellViewModel): void; + revealElementLineInView(element: ICellViewModel, line: number): void; + revealElementLineInCenter(element: ICellViewModel, line: number): void; + revealElementLineInCenterIfOutsideViewport(element: ICellViewModel, line: number): void; + revealElementRangeInView(element: ICellViewModel, range: Range): void; + revealElementRangeInCenter(element: ICellViewModel, range: Range): void; + revealElementRangeInCenterIfOutsideViewport(element: ICellViewModel, range: Range): void; + setHiddenAreas(_ranges: ICellRange[]): boolean; + domElementOfElement(element: ICellViewModel): HTMLElement | null; + focusView(): void; + getAbsoluteTopOfElement(element: ICellViewModel): number; + triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void; + updateElementHeight2(element: ICellViewModel, size: number): void; + domFocus(): void; + setCellSelection(element: ICellViewModel, range: Range): void; + style(styles: IListStyles): void; + updateOptions(options: IListOptions): void; + layout(height?: number, width?: number): void; + dispose(): void; +} + export interface BaseCellRenderTemplate { container: HTMLElement; cellContainer: HTMLElement; @@ -352,3 +400,66 @@ export enum CursorAtBoundary { Bottom, Both } + +/** + * [start, start + length] + */ +export interface ICellRange { + start: number; + length: number; +} + + +/** + * @param _ranges + */ +export function reduceCellRanges(_ranges: ICellRange[]): ICellRange[] { + if (!_ranges.length) { + return []; + } + + let ranges = _ranges.sort((a, b) => a.start - b.start); + let result: ICellRange[] = []; + let currentRangeStart = ranges[0].start; + let currentRangeEnd = ranges[0].start + ranges[0].length; + + for (let i = 0, len = ranges.length; i < len; i++) { + let range = ranges[i]; + + if (range.start > currentRangeEnd) { + result.push({ start: currentRangeStart, length: currentRangeEnd - currentRangeStart }); + currentRangeStart = range.start; + currentRangeEnd = range.start + range.length; + } else if (range.start + range.length > currentRangeEnd) { + currentRangeEnd = range.start + range.length; + } + } + + result.push({ start: currentRangeStart, length: currentRangeEnd - currentRangeStart }); + return result; +} + +export function getVisibleCells(cells: CellViewModel[], hiddenRanges: ICellRange[]) { + if (!hiddenRanges.length) { + return cells; + } + + let start = 0; + let hiddenRangeIndex = 0; + let result: any[] = []; + + while (start < cells.length && hiddenRangeIndex < hiddenRanges.length) { + if (start < hiddenRanges[hiddenRangeIndex].start) { + result.push(...cells.slice(start, hiddenRanges[hiddenRangeIndex].start)); + } + + start = hiddenRanges[hiddenRangeIndex].start + hiddenRanges[hiddenRangeIndex].length; + hiddenRangeIndex++; + } + + if (start < cells.length) { + result.push(...cells.slice(start)); + } + + return result; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 5037f478aa27b4a6d301f53106aa82b2d88a3ccd..f37098e51c9dd8d6c2f6c2bab2f15043ca1678e5 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -30,7 +30,7 @@ import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorOptions, IEditorCloseEvent, IEditorMemento } from 'vs/workbench/common/editor'; import { CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_TOP_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; import { NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget'; -import { CellEditState, CellFocusMode, ICellViewModel, INotebookEditor, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, ICellViewModel, INotebookEditor, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, INotebookCellList, ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorInput, NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; @@ -70,7 +70,7 @@ export class NotebookCodeEditors implements ICompositeCodeEditor { readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; constructor( - private _list: NotebookCellList, + private _list: INotebookCellList, private _renderedEditors: Map ) { _list.onDidChangeFocus(_e => this._onDidChangeActiveEditor.fire(this), undefined, this._disposables); @@ -93,7 +93,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { private body!: HTMLElement; private webview: BackLayerWebView | null = null; private webviewTransparentCover: HTMLElement | null = null; - private list: NotebookCellList | undefined; + private list: INotebookCellList | undefined; private control: ICompositeCodeEditor | undefined; private renderedEditors: Map = new Map(); private eventDispatcher: NotebookEventDispatcher | undefined; @@ -275,7 +275,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.webview = null; } - this.list?.splice(0, this.list?.length); + this.list?.clear(); super.onHide(); } @@ -348,12 +348,13 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { private detachModel() { this.localStore.clear(); + this.list?.detachViewModel(); this.notebookViewModel?.dispose(); this.notebookViewModel = undefined; this.webview?.clearInsets(); this.webview?.clearPreloadsCache(); this.findWidget.clear(); - this.list?.splice(0, this.list?.length || 0); + this.list?.clear(); } private async attachModel(input: NotebookEditorInput, model: NotebookEditorModel) { @@ -373,36 +374,6 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.editorEditable?.set(e.source.editable); })); - this.localStore.add(this.notebookViewModel.onDidChangeViewCells((e) => { - if (e.synchronous) { - e.splices.reverse().forEach((diff) => { - // remove output in the webview - for (let i = diff[0]; i < diff[0] + diff[1]; i++) { - const cell = this.list?.element(i); - cell?.model.outputs.forEach(output => { - this.removeInset(output); - }); - } - - this.list?.splice(diff[0], diff[1], diff[2]); - }); - } else { - DOM.scheduleAtNextAnimationFrame(() => { - e.splices.reverse().forEach((diff) => { - // remove output in the webview - for (let i = diff[0]; i < diff[0] + diff[1]; i++) { - const cell = this.list?.element(i); - cell?.model.outputs.forEach(output => { - this.removeInset(output); - }); - } - - this.list?.splice(diff[0], diff[1], diff[2]); - }); - }); - } - })); - this.webview?.updateRendererPreloads(this.notebookViewModel.renderers); this.localStore.add(this.list!.onWillScroll(e => { @@ -418,9 +389,8 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { if (this.webview?.insetMapping) { this.webview?.insetMapping.forEach((value, key) => { - let cell = value.cell; - let index = this.notebookViewModel!.getViewCellIndex(cell); - let cellTop = this.list?.getAbsoluteTop(index) || 0; + const cell = value.cell; + const cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; if (this.webview!.shouldUpdateInset(cell, key, cellTop)) { updateItems.push({ cell: cell, @@ -436,8 +406,12 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } })); - this.list?.splice(0, 0, this.notebookViewModel!.viewCells as CellViewModel[]); - this.list?.layout(); + this.list!.attachViewModel(this.notebookViewModel); + this.localStore.add(this.list!.onDidRemoveOutput(output => { + this.removeInset(output); + })); + + this.list!.layout(); if (viewState?.scrollPosition !== undefined) { this.list!.scrollTop = viewState!.scrollPosition.top; @@ -454,8 +428,8 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { if (this.list) { state.scrollPosition = { left: this.list.scrollLeft, top: this.list.scrollTop }; let cellHeights: { [key: number]: number } = {}; - for (let i = 0; i < this.list.length; i++) { - const elm = this.list.element(i)!; + for (let i = 0; i < this.viewModel!.viewCells.length; i++) { + const elm = this.viewModel!.viewCells[i] as CellViewModel; if (elm.cellKind === CellKind.Code) { cellHeights[i] = elm.layoutInfo.totalHeight; } else { @@ -507,98 +481,57 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { //#region Editor Features selectElement(cell: ICellViewModel) { - const index = this.notebookViewModel?.getViewCellIndex(cell); - - if (index !== undefined) { - this.list?.setSelection([index]); - this.list?.setFocus([index]); - } + this.list?.selectElement(cell); } revealInView(cell: ICellViewModel) { - const index = this.notebookViewModel?.getViewCellIndex(cell); - - if (index !== undefined) { - this.list?.revealInView(index); - } + this.list?.revealElementInView(cell); } revealInCenterIfOutsideViewport(cell: ICellViewModel) { - const index = this.notebookViewModel?.getViewCellIndex(cell); - - if (index !== undefined) { - this.list?.revealInCenterIfOutsideViewport(index); - } + this.list?.revealElementInCenterIfOutsideViewport(cell); } revealInCenter(cell: ICellViewModel) { - const index = this.notebookViewModel?.getViewCellIndex(cell); - - if (index !== undefined) { - this.list?.revealInCenter(index); - } + this.list?.revealElementInCenter(cell); } revealLineInView(cell: ICellViewModel, line: number): void { - const index = this.notebookViewModel?.getViewCellIndex(cell); - - if (index !== undefined) { - this.list?.revealLineInView(index, line); - } + this.list?.revealElementLineInView(cell, line); } revealLineInCenter(cell: ICellViewModel, line: number) { - const index = this.notebookViewModel?.getViewCellIndex(cell); - - if (index !== undefined) { - this.list?.revealLineInCenter(index, line); - } + this.list?.revealElementLineInCenter(cell, line); } revealLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number) { - const index = this.notebookViewModel?.getViewCellIndex(cell); - - if (index !== undefined) { - this.list?.revealLineInCenterIfOutsideViewport(index, line); - } + this.list?.revealElementLineInCenterIfOutsideViewport(cell, line); } revealRangeInView(cell: ICellViewModel, range: Range): void { - const index = this.notebookViewModel?.getViewCellIndex(cell); - - if (index !== undefined) { - this.list?.revealRangeInView(index, range); - } + this.list?.revealElementRangeInView(cell, range); } revealRangeInCenter(cell: ICellViewModel, range: Range): void { - const index = this.notebookViewModel?.getViewCellIndex(cell); - - if (index !== undefined) { - this.list?.revealRangeInCenter(index, range); - } + this.list?.revealElementRangeInCenter(cell, range); } revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void { - const index = this.notebookViewModel?.getViewCellIndex(cell); - - if (index !== undefined) { - this.list?.revealRangeInCenterIfOutsideViewport(index, range); - } + this.list?.revealElementRangeInCenterIfOutsideViewport(cell, range); } setCellSelection(cell: ICellViewModel, range: Range): void { - const index = this.notebookViewModel?.getViewCellIndex(cell); - - if (index !== undefined) { - this.list?.setCellSelection(index, range); - } + this.list?.setCellSelection(cell, range); } changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any { return this.notebookViewModel?.changeDecorations(callback); } + setHiddenAreas(_ranges: ICellRange[]): boolean { + return this.list!.setHiddenAreas(_ranges); + } + //#endregion //#region Find Delegate @@ -617,10 +550,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { //#region Cell operations async layoutNotebookCell(cell: ICellViewModel, height: number): Promise { let relayout = (cell: ICellViewModel, height: number) => { - let index = this.notebookViewModel!.getViewCellIndex(cell); - if (index >= 0) { - this.list?.updateElementHeight(index, height); - } + this.list?.updateElementHeight2(cell, height); }; let r: () => void; @@ -635,10 +565,10 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { async insertNotebookCell(cell: ICellViewModel, type: CellKind, direction: 'above' | 'below', initialText: string = ''): Promise { const newLanguages = this.notebookViewModel!.languages; const language = newLanguages && newLanguages.length ? newLanguages[0] : 'markdown'; - const index = this.notebookViewModel!.getViewCellIndex(cell); + const index = this.notebookViewModel!.getCellIndex(cell); const insertIndex = direction === 'above' ? index : index + 1; const newCell = this.notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, true); - this.list?.setFocus([insertIndex]); + this.list?.focusElement(newCell); if (type === CellKind.Markdown) { newCell.editState = CellEditState.Editing; @@ -646,7 +576,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { let r: () => void; DOM.scheduleAtNextAnimationFrame(() => { - this.list?.revealInCenterIfOutsideViewport(insertIndex); + this.list?.revealElementInCenterIfOutsideViewport(cell); r(); }); @@ -655,12 +585,12 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { async deleteNotebookCell(cell: ICellViewModel): Promise { (cell as CellViewModel).save(); - const index = this.notebookViewModel!.getViewCellIndex(cell); + const index = this.notebookViewModel!.getCellIndex(cell); this.notebookViewModel!.deleteCell(index, true); } async moveCellDown(cell: ICellViewModel): Promise { - const index = this.notebookViewModel!.getViewCellIndex(cell); + const index = this.notebookViewModel!.getCellIndex(cell); if (index === this.notebookViewModel!.viewCells.length - 1) { return; } @@ -670,7 +600,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } async moveCellUp(cell: ICellViewModel): Promise { - const index = this.notebookViewModel!.getViewCellIndex(cell); + const index = this.notebookViewModel!.getCellIndex(cell); if (index === 0) { return; } @@ -686,7 +616,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { let r: () => void; DOM.scheduleAtNextAnimationFrame(() => { - this.list?.revealInCenterIfOutsideViewport(index + 1); + this.list?.revealElementInCenterIfOutsideViewport(this.notebookViewModel!.viewCells[index + 1]); r(); }); @@ -785,18 +715,15 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } focusNotebookCell(cell: ICellViewModel, focusEditor: boolean) { - const index = this.notebookViewModel!.getViewCellIndex(cell); - if (focusEditor) { - this.list?.setFocus([index]); - this.list?.setSelection([index]); + this.selectElement(cell); this.list?.focusView(); cell.editState = CellEditState.Editing; cell.focusMode = CellFocusMode.Editor; this.revealInCenterIfOutsideViewport(cell); } else { - let itemDOM = this.list?.domElementAtIndex(index); + let itemDOM = this.list?.domElementOfElement(cell); if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { (document.activeElement as HTMLElement).blur(); } @@ -804,8 +731,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { cell.editState = CellEditState.Preview; cell.focusMode = CellFocusMode.Editor; - this.list?.setFocus([index]); - this.list?.setSelection([index]); + this.selectElement(cell); this.revealInCenterIfOutsideViewport(cell); this.list?.focusView(); } @@ -839,13 +765,10 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { let preloads = this.notebookViewModel!.renderers; if (!this.webview!.insetMapping.has(output)) { - let index = this.notebookViewModel!.getViewCellIndex(cell); - let cellTop = this.list?.getAbsoluteTop(index) || 0; - + let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; this.webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); } else { - let index = this.notebookViewModel!.getViewCellIndex(cell); - let cellTop = this.list?.getAbsoluteTop(index) || 0; + let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; let scrollTop = this.list?.scrollTop || 0; this.webview!.updateViewScrollTop(-scrollTop, [{ cell: cell, output: output, cellTop: cellTop }]); @@ -959,13 +882,13 @@ registerThemingParticipant((theme, collector) => { } // Cell Margin - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > div.cell { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row { padding-top: ${EDITOR_TOP_MARGIN}px; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN}px; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { padding-top: ${EDITOR_TOP_MARGIN}px; }`); collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container { width: calc(100% - ${CELL_MARGIN * 2 + CELL_RUN_GUTTER}px); margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .cell-editor-container { width: calc(100% - ${CELL_RUN_GUTTER}px); }`); collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .markdown-editor-container { margin-left: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .run-button-container { width: ${CELL_RUN_GUTTER}px; }`); }); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 7e9c4102ab40044ac12b0111cba9e94bf77e5a56..d329b3de3eb018c2b23f93574eb2f4cf3724e261 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -5,7 +5,7 @@ import * as DOM from 'vs/base/browser/dom'; import { IListRenderer, IListVirtualDelegate, ListError } from 'vs/base/browser/ui/list/list'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -14,14 +14,16 @@ import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { isMacintosh } from 'vs/base/common/platform'; -import { NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NOTEBOOK_EDITOR_CURSOR_BOUNDARY, IOutput, diff } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Range } from 'vs/editor/common/core/range'; -import { CellRevealType, CellRevealPosition, CursorAtBoundary } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealType, CellRevealPosition, CursorAtBoundary, ICellViewModel, INotebookCellList, ICellRange, reduceCellRanges, getVisibleCells } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IStyleController, IListStyles } from 'vs/base/browser/ui/list/listWidget'; +import { TrackedRangeStickiness } from 'vs/editor/common/model'; +import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -export class NotebookCellList extends WorkbenchList implements IDisposable, IStyleController { +export class NotebookCellList extends WorkbenchList implements IDisposable, IStyleController, INotebookCellList { get onWillScroll(): Event { return this.view.onWillScroll; } get rowsContainer(): HTMLElement { @@ -29,7 +31,16 @@ export class NotebookCellList extends WorkbenchList implements ID } private _previousSelectedElements: CellViewModel[] = []; private _localDisposableStore = new DisposableStore(); + private _viewModelStore = new DisposableStore(); private styleElement?: HTMLStyleElement; + + private readonly _onDidRemoveOutput = new Emitter(); + readonly onDidRemoveOutput: Event = this._onDidRemoveOutput.event; + + private _viewModel: NotebookViewModel | null = null; + private _hiddenRangeIds: string[] = []; + private hiddenRangesPrefixSum: PrefixSumComputer | null = null; + constructor( private listUser: string, container: HTMLElement, @@ -107,38 +118,293 @@ export class NotebookCellList extends WorkbenchList implements ID } - domElementAtIndex(index: number): HTMLElement | null { - return this.view.domElement(index); + detachViewModel() { + this._viewModelStore.clear(); + this._viewModel = null; } - focusView() { - this.view.domNode.focus(); + attachViewModel(model: NotebookViewModel) { + this._viewModel = model; + this._viewModelStore.add(model.onDidChangeViewCells((e) => { + const currentRanges = this._hiddenRangeIds.map(id => this._viewModel!.getTrackedRange(id)).filter(range => range !== null) as ICellRange[]; + const newVisibleViewCells: CellViewModel[] = getVisibleCells(this._viewModel!.viewCells as CellViewModel[], currentRanges); + + const oldVisibleViewCells: CellViewModel[] = []; + const oldViewCellMapping = new Set(); + for (let i = 0; i < this.length; i++) { + oldVisibleViewCells.push(this.element(i)); + oldViewCellMapping.add(this.element(i).uri.toString()); + } + + const viewDiffs = diff(oldVisibleViewCells, newVisibleViewCells, a => { + return oldViewCellMapping.has(a.uri.toString()); + }); + + if (e.synchronous) { + viewDiffs.reverse().forEach((diff) => { + // remove output in the webview + for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { + const cell = this.element(i); + cell?.model.outputs.forEach(output => { + this._onDidRemoveOutput.fire(output); + }); + } + + this.splice2(diff.start, diff.deleteCount, diff.toInsert); + }); + } else { + DOM.scheduleAtNextAnimationFrame(() => { + viewDiffs.reverse().forEach((diff) => { + // remove output in the webview + for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { + const cell = this.element(i); + cell?.model.outputs.forEach(output => { + this._onDidRemoveOutput.fire(output); + }); + } + + this.splice2(diff.start, diff.deleteCount, diff.toInsert); + }); + }); + } + })); + + this.splice2(0, 0, model.viewCells as CellViewModel[]); } - getAbsoluteTop(index: number): number { - if (index < 0 || index >= this.length) { - throw new ListError(this.listUser, `Invalid index ${index}`); + clear() { + super.splice(0, this.length); + } + + setHiddenAreas(_ranges: ICellRange[]): boolean { + const newRanges = reduceCellRanges(_ranges); + // delete old tracking ranges + const oldRanges = this._hiddenRangeIds.map(id => this._viewModel!.getTrackedRange(id)).filter(range => range !== null) as ICellRange[]; + if (newRanges.length === oldRanges.length) { + let hasDifference = false; + for (let i = 0; i < newRanges.length; i++) { + if (!(newRanges[i].start === oldRanges[i].start && newRanges[i].length === oldRanges[i].length)) { + hasDifference = true; + break; + } + } + + if (!hasDifference) { + return false; + } } - return this.view.elementTop(index); + this._hiddenRangeIds.forEach(id => this._viewModel!.setTrackedRange(id, null, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter)); + const hiddenAreaIds = newRanges.map(range => this._viewModel!.setTrackedRange(null, range, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter)).filter(id => id !== null) as string[]; + + this._hiddenRangeIds = hiddenAreaIds; + + this.updateHiddenAreasInView(oldRanges, newRanges); + + // set hidden ranges prefix sum + let start = 0; + let index = 0; + let ret: number[] = []; + + while (index < newRanges.length) { + for (let j = start; j < newRanges[index].start - 1; j++) { + ret.push(1); + } + + ret.push(newRanges[index].length + 1); + start = newRanges[index].start + newRanges[index].length; + index++; + } + + for (let i = start; i < this._viewModel!.viewCells.length; i++) { + ret.push(1); + } + + const values = new Uint32Array(ret.length); + for (let i = 0; i < ret.length; i++) { + values[i] = ret[i]; + } + + this.hiddenRangesPrefixSum = new PrefixSumComputer(values); + // console.log(ret); + // for (let i = 0; i < this.hiddenRangesPrefixSum.getCount(); i++) { + // console.log(this.hiddenRangesPrefixSum.getAccumulatedValue(i)); + // } + + // for (let i = 0; i < this._viewModel!.viewCells.length; i++) { + // console.log(this.hiddenRangesPrefixSum.getIndexOf(i)); + // } + + return true; + } + + /** + * oldRanges and newRanges are all reduced and sorted. + */ + updateHiddenAreasInView(oldRanges: ICellRange[], newRanges: ICellRange[]) { + const oldViewCellEntries: CellViewModel[] = getVisibleCells(this._viewModel!.viewCells as CellViewModel[], oldRanges); + const oldViewCellMapping = new Set(); + oldViewCellEntries.forEach(cell => { + oldViewCellMapping.add(cell.uri.toString()); + }); + + const newViewCellEntries: CellViewModel[] = getVisibleCells(this._viewModel!.viewCells as CellViewModel[], newRanges); + + const viewDiffs = diff(oldViewCellEntries, newViewCellEntries, a => { + return oldViewCellMapping.has(a.uri.toString()); + }); + + viewDiffs.reverse().forEach((diff) => { + // remove output in the webview + for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { + const cell = this.element(i); + cell?.model.outputs.forEach(output => { + this._onDidRemoveOutput.fire(output); + }); + } + + this.splice2(diff.start, diff.deleteCount, diff.toInsert); + }); + } + + splice2(start: number, deleteCount: number, elements: CellViewModel[] = []): void { + // we need to convert start and delete count based on hidden ranges + super.splice(start, deleteCount, elements); + } + + + getViewIndex(cell: ICellViewModel) { + const modelIndex = this._viewModel!.getCellIndex(cell); + if (!this.hiddenRangesPrefixSum) { + return modelIndex; + } + + return this.hiddenRangesPrefixSum.getIndexOf(modelIndex).index; + } + + focusElement(cell: ICellViewModel) { + const index = this.getViewIndex(cell); + + if (index !== undefined) { + this.setFocus([index]); + } + } + selectElement(cell: ICellViewModel) { + const index = this.getViewIndex(cell); + + if (index !== undefined) { + this.setSelection([index]); + this.setFocus([index]); + } + } + + revealElementInView(cell: ICellViewModel) { + const index = this.getViewIndex(cell); + + if (index !== undefined) { + this._revealInView(index); + } + } + + revealElementInCenterIfOutsideViewport(cell: ICellViewModel) { + const index = this.getViewIndex(cell); + + if (index !== undefined) { + this._revealInCenterIfOutsideViewport(index); + } + } + + revealElementInCenter(cell: ICellViewModel) { + const index = this.getViewIndex(cell); + + if (index !== undefined) { + this._revealInCenter(index); + } + } + + revealElementLineInView(cell: ICellViewModel, line: number): void { + const index = this.getViewIndex(cell); + + if (index !== undefined) { + this._revealLineInView(index, line); + } + } + + revealElementLineInCenter(cell: ICellViewModel, line: number) { + const index = this.getViewIndex(cell); + + if (index !== undefined) { + this._revealLineInCenter(index, line); + } + } + + revealElementLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number) { + const index = this.getViewIndex(cell); + + if (index !== undefined) { + this._revealLineInCenterIfOutsideViewport(index, line); + } + } + + revealElementRangeInView(cell: ICellViewModel, range: Range): void { + const index = this.getViewIndex(cell); + + if (index !== undefined) { + this._revealRangeInView(index, range); + } + } + + revealElementRangeInCenter(cell: ICellViewModel, range: Range): void { + const index = this.getViewIndex(cell); + + if (index !== undefined) { + this._revealRangeInCenter(index, range); + } + } + + revealElementRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void { + const index = this.getViewIndex(cell); + + if (index !== undefined) { + this._revealRangeInCenterIfOutsideViewport(index, range); + } } - getElementHeight(index: number): number { - if (index < 0 || index >= this.length) { + domElementOfElement(element: ICellViewModel): HTMLElement | null { + const index = this.getViewIndex(element); + if (index !== undefined) { + return this.view.domElement(index); + } + + return null; + } + + focusView() { + this.view.domNode.focus(); + } + + getAbsoluteTopOfElement(element: ICellViewModel): number { + let index = this.getViewIndex(element); + if (index === undefined || index < 0 || index >= this.length) { throw new ListError(this.listUser, `Invalid index ${index}`); } - return this.view.elementHeight(index); + return this.view.elementTop(index); } triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { this.view.triggerScrollFromMouseWheelEvent(browserEvent); } - updateElementHeight(index: number, size: number): void { + + updateElementHeight2(element: ICellViewModel, size: number): void { + const index = this.getViewIndex(element); + if (index === undefined) { + return; + } + const focused = this.getSelection(); this.view.updateElementHeight(index, size, focused.length ? focused[0] : null); - // this.view.updateElementHeight(index, size, null); } // override @@ -155,13 +421,13 @@ export class NotebookCellList extends WorkbenchList implements ID super.domFocus(); } - private _revealRange(index: number, range: Range, revealType: CellRevealType, newlyCreated: boolean, alignToBottom: boolean) { - const element = this.view.element(index); + private _revealRange(viewIndex: number, range: Range, revealType: CellRevealType, newlyCreated: boolean, alignToBottom: boolean) { + const element = this.view.element(viewIndex); const scrollTop = this.view.getScrollTop(); const wrapperBottom = scrollTop + this.view.renderHeight; const startLineNumber = range.startLineNumber; const lineOffset = element.getLineScrollTopOffset(startLineNumber); - const elementTop = this.view.elementTop(index); + const elementTop = this.view.elementTop(viewIndex); const lineTop = elementTop + lineOffset; // TODO@rebornix 30 ---> line height * 1.5 @@ -189,16 +455,16 @@ export class NotebookCellList extends WorkbenchList implements ID // List items have real dynamic heights, which means after we set `scrollTop` based on the `elementTop(index)`, the element at `index` might still be removed from the view once all relayouting tasks are done. // For example, we scroll item 10 into the view upwards, in the first round, items 7, 8, 9, 10 are all in the viewport. Then item 7 and 8 resize themselves to be larger and finally item 10 is removed from the view. // To ensure that item 10 is always there, we need to scroll item 10 to the top edge of the viewport. - private _revealRangeInternal(index: number, range: Range, revealType: CellRevealType) { + private _revealRangeInternal(viewIndex: number, range: Range, revealType: CellRevealType) { const scrollTop = this.view.getScrollTop(); const wrapperBottom = scrollTop + this.view.renderHeight; - const elementTop = this.view.elementTop(index); - const element = this.view.element(index); + const elementTop = this.view.elementTop(viewIndex); + const element = this.view.element(viewIndex); if (element.editorAttached) { - this._revealRange(index, range, revealType, false, false); + this._revealRange(viewIndex, range, revealType, false, false); } else { - const elementHeight = this.view.elementHeight(index); + const elementHeight = this.view.elementHeight(viewIndex); let upwards = false; if (elementTop + elementHeight < scrollTop) { @@ -216,24 +482,24 @@ export class NotebookCellList extends WorkbenchList implements ID }); editorAttachedPromise.then(() => { - this._revealRange(index, range, revealType, true, upwards); + this._revealRange(viewIndex, range, revealType, true, upwards); }); } } - revealLineInView(index: number, line: number) { - this._revealRangeInternal(index, new Range(line, 1, line, 1), CellRevealType.Line); + private _revealLineInView(viewIndex: number, line: number) { + this._revealRangeInternal(viewIndex, new Range(line, 1, line, 1), CellRevealType.Line); } - revealRangeInView(index: number, range: Range): void { - this._revealRangeInternal(index, range, CellRevealType.Range); + private _revealRangeInView(viewIndex: number, range: Range): void { + this._revealRangeInternal(viewIndex, range, CellRevealType.Range); } - private _revealRangeInCenterInternal(index: number, range: Range, revealType: CellRevealType) { - const reveal = (index: number, range: Range, revealType: CellRevealType) => { - const element = this.view.element(index); + private _revealRangeInCenterInternal(viewIndex: number, range: Range, revealType: CellRevealType) { + const reveal = (viewIndex: number, range: Range, revealType: CellRevealType) => { + const element = this.view.element(viewIndex); let lineOffset = element.getLineScrollTopOffset(range.startLineNumber); - let lineOffsetInView = this.view.elementTop(index) + lineOffset; + let lineOffsetInView = this.view.elementTop(viewIndex) + lineOffset; this.view.setScrollTop(lineOffsetInView - this.view.renderHeight / 2); if (revealType === CellRevealType.Range) { @@ -241,31 +507,31 @@ export class NotebookCellList extends WorkbenchList implements ID } }; - const elementTop = this.view.elementTop(index); + const elementTop = this.view.elementTop(viewIndex); const viewItemOffset = elementTop; this.view.setScrollTop(viewItemOffset - this.view.renderHeight / 2); - const element = this.view.element(index); + const element = this.view.element(viewIndex); if (!element.editorAttached) { - getEditorAttachedPromise(element).then(() => reveal(index, range, revealType)); + getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType)); } else { - reveal(index, range, revealType); + reveal(viewIndex, range, revealType); } } - revealLineInCenter(index: number, line: number) { - this._revealRangeInCenterInternal(index, new Range(line, 1, line, 1), CellRevealType.Line); + private _revealLineInCenter(viewIndex: number, line: number) { + this._revealRangeInCenterInternal(viewIndex, new Range(line, 1, line, 1), CellRevealType.Line); } - revealRangeInCenter(index: number, range: Range): void { - this._revealRangeInCenterInternal(index, range, CellRevealType.Range); + private _revealRangeInCenter(viewIndex: number, range: Range): void { + this._revealRangeInCenterInternal(viewIndex, range, CellRevealType.Range); } - private _revealRangeInCenterIfOutsideViewportInternal(index: number, range: Range, revealType: CellRevealType) { - const reveal = (index: number, range: Range, revealType: CellRevealType) => { - const element = this.view.element(index); + private _revealRangeInCenterIfOutsideViewportInternal(viewIndex: number, range: Range, revealType: CellRevealType) { + const reveal = (viewIndex: number, range: Range, revealType: CellRevealType) => { + const element = this.view.element(viewIndex); let lineOffset = element.getLineScrollTopOffset(range.startLineNumber); - let lineOffsetInView = this.view.elementTop(index) + lineOffset; + let lineOffsetInView = this.view.elementTop(viewIndex) + lineOffset; this.view.setScrollTop(lineOffsetInView - this.view.renderHeight / 2); if (revealType === CellRevealType.Range) { @@ -275,21 +541,21 @@ export class NotebookCellList extends WorkbenchList implements ID const scrollTop = this.view.getScrollTop(); const wrapperBottom = scrollTop + this.view.renderHeight; - const elementTop = this.view.elementTop(index); + const elementTop = this.view.elementTop(viewIndex); const viewItemOffset = elementTop; - const element = this.view.element(index); + const element = this.view.element(viewIndex); if (viewItemOffset < scrollTop || viewItemOffset > wrapperBottom) { // let it render this.view.setScrollTop(viewItemOffset - this.view.renderHeight / 2); // after rendering, it might be pushed down due to markdown cell dynamic height - const elementTop = this.view.elementTop(index); + const elementTop = this.view.elementTop(viewIndex); this.view.setScrollTop(elementTop - this.view.renderHeight / 2); // reveal editor if (!element.editorAttached) { - getEditorAttachedPromise(element).then(() => reveal(index, range, revealType)); + getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType)); } else { // for example markdown } @@ -298,27 +564,27 @@ export class NotebookCellList extends WorkbenchList implements ID element.revealRangeInCenter(range); } else { // for example, markdown cell in preview mode - getEditorAttachedPromise(element).then(() => reveal(index, range, revealType)); + getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType)); } } } - revealLineInCenterIfOutsideViewport(index: number, line: number) { - this._revealRangeInCenterIfOutsideViewportInternal(index, new Range(line, 1, line, 1), CellRevealType.Line); + private _revealLineInCenterIfOutsideViewport(viewIndex: number, line: number) { + this._revealRangeInCenterIfOutsideViewportInternal(viewIndex, new Range(line, 1, line, 1), CellRevealType.Line); } - revealRangeInCenterIfOutsideViewport(index: number, range: Range): void { - this._revealRangeInCenterIfOutsideViewportInternal(index, range, CellRevealType.Range); + private _revealRangeInCenterIfOutsideViewport(viewIndex: number, range: Range): void { + this._revealRangeInCenterIfOutsideViewportInternal(viewIndex, range, CellRevealType.Range); } - private _revealInternal(index: number, ignoreIfInsideViewport: boolean, revealPosition: CellRevealPosition) { - if (index >= this.view.length) { + private _revealInternal(viewIndex: number, ignoreIfInsideViewport: boolean, revealPosition: CellRevealPosition) { + if (viewIndex >= this.view.length) { return; } const scrollTop = this.view.getScrollTop(); const wrapperBottom = scrollTop + this.view.renderHeight; - const elementTop = this.view.elementTop(index); + const elementTop = this.view.elementTop(viewIndex); if (ignoreIfInsideViewport && elementTop >= scrollTop && elementTop < wrapperBottom) { // inside the viewport @@ -330,25 +596,25 @@ export class NotebookCellList extends WorkbenchList implements ID this.view.setScrollTop(viewItemOffset); // second scroll as markdown cell is dynamic - const newElementTop = this.view.elementTop(index); + const newElementTop = this.view.elementTop(viewIndex); const newViewItemOffset = revealPosition === CellRevealPosition.Top ? newElementTop : (newElementTop - this.view.renderHeight / 2); this.view.setScrollTop(newViewItemOffset); } - revealInView(index: number) { - this._revealInternal(index, true, CellRevealPosition.Top); + private _revealInView(viewIndex: number) { + this._revealInternal(viewIndex, true, CellRevealPosition.Top); } - revealInCenter(index: number) { - this._revealInternal(index, false, CellRevealPosition.Center); + private _revealInCenter(viewIndex: number) { + this._revealInternal(viewIndex, false, CellRevealPosition.Center); } - revealInCenterIfOutsideViewport(index: number) { - this._revealInternal(index, true, CellRevealPosition.Center); + private _revealInCenterIfOutsideViewport(viewIndex: number) { + this._revealInternal(viewIndex, true, CellRevealPosition.Center); } - setCellSelection(index: number, range: Range) { - const element = this.view.element(index); + setCellSelection(cell: ICellViewModel, range: Range) { + const element = cell as CellViewModel; if (element.editorAttached) { element.setSelection(range); } else { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 7733cf12ff21a7f24d001fdc5e3a4cdf65bf3798..f3c8121062e450c7b6f7ef74d0850b6497ba9010 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -343,7 +343,11 @@ export class BackLayerWebView extends Disposable { // const top = data.data.top; // console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); } else if (data.type === 'did-scroll-wheel') { - this.notebookEditor.triggerScroll(data.payload); + this.notebookEditor.triggerScroll({ + ...data.payload, + preventDefault: () => { }, + stopPropagation: () => { } + }); } return; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 34d4b4a8ff63a32f43eebfce34c192a1a33ac680..47aefbe9bbb089c4c4b4f4c864c271375e78d057 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -110,7 +110,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod const outputTotalHeight = this._outputsTop!.getTotalValue(); const totalHeight = EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_MARGIN + outputTotalHeight + BOTTOM_CELL_TOOLBAR_HEIGHT + CELL_STATUSBAR_HEIGHT; const indicatorHeight = this.editorHeight + CELL_STATUSBAR_HEIGHT + outputTotalHeight; - const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN + this.editorHeight; + const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN + this.editorHeight + CELL_STATUSBAR_HEIGHT; const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT; const editorWidth = state.outerWidth !== undefined ? state.outerWidth - CELL_MARGIN * 2 - CELL_RUN_GUTTER : this._layoutInfo?.editorWidth; this._layoutInfo = { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 7da92b3f0d5204fdde141fc83ad90af154542865..72fc8003eff4ab84d83de6a9fdc51431d14cafb0 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -10,11 +10,11 @@ import { URI } from 'vs/base/common/uri'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IModelDeltaDecoration, TrackedRangeStickiness, IModelDecorationOptions } from 'vs/editor/common/model'; import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { CellFindMatch, CellEditState, ICellViewModel, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellFindMatch, CellEditState, ICellViewModel, NotebookLayoutInfo, ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { DeleteCellEdit, InsertCellEdit, MoveCellEdit } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEdit'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; @@ -23,6 +23,7 @@ import { CellKind, ICell } from 'vs/workbench/contrib/notebook/common/notebookCo import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import * as strings from 'vs/base/common/strings'; export interface INotebookEditorViewState { editingCells: { [key: number]: boolean }; @@ -59,6 +60,70 @@ export interface INotebookViewCellsUpdateEvent { splices: NotebookViewCellsSplice[]; } +import { IntervalTree, IntervalNode } from 'vs/editor/common/model/intervalTree'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; + +class DecorationsTree { + private readonly _decorationsTree: IntervalTree; + + constructor() { + this._decorationsTree = new IntervalTree(); + } + + public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { + const r1 = this._decorationsTree.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId); + return r1; + } + + public search(filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] { + return this._decorationsTree.search(filterOwnerId, filterOutValidation, cachedVersionId); + + } + + public collectNodesFromOwner(ownerId: number): IntervalNode[] { + const r1 = this._decorationsTree.collectNodesFromOwner(ownerId); + return r1; + } + + public collectNodesPostOrder(): IntervalNode[] { + const r1 = this._decorationsTree.collectNodesPostOrder(); + return r1; + } + + public insert(node: IntervalNode): void { + this._decorationsTree.insert(node); + } + + public delete(node: IntervalNode): void { + this._decorationsTree.delete(node); + } + + public resolveNode(node: IntervalNode, cachedVersionId: number): void { + this._decorationsTree.resolveNode(node, cachedVersionId); + } + + public acceptReplace(offset: number, length: number, textLength: number, forceMoveMarkers: boolean): void { + this._decorationsTree.acceptReplace(offset, length, textLength, forceMoveMarkers); + } +} + +const TRACKED_RANGE_OPTIONS = [ + ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }), + ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }), + ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.GrowsOnlyWhenTypingBefore }), + ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.GrowsOnlyWhenTypingAfter }), +]; + +function _normalizeOptions(options: IModelDecorationOptions): ModelDecorationOptions { + if (options instanceof ModelDecorationOptions) { + return options; + } + return ModelDecorationOptions.createDynamic(options); +} + +let MODEL_ID = 0; + + export class NotebookViewModel extends Disposable { private _localStore: DisposableStore = this._register(new DisposableStore()); private _viewCells: CellViewModel[] = []; @@ -117,6 +182,12 @@ export class NotebookViewModel extends Disposable { return this._layoutInfo; } + private _decorationsTree = new DecorationsTree(); + private _decorations: { [decorationId: string]: IntervalNode; } = Object.create(null); + private _lastDecorationId: number = 0; + private readonly _instanceId: string; + public readonly id: string; + constructor( public viewType: string, private _model: NotebookEditorModel, @@ -128,6 +199,10 @@ export class NotebookViewModel extends Disposable { ) { super(); + MODEL_ID++; + this.id = '$notebookViewModel' + MODEL_ID; + this._instanceId = strings.singleLetterHash(MODEL_ID); + // this._register(this._model.onDidChangeCells(e => { // this._onDidChangeViewCells.fire({ // synchronous: true, @@ -164,10 +239,123 @@ export class NotebookViewModel extends Disposable { }); } - getViewCellIndex(cell: ICellViewModel) { + getCellIndex(cell: ICellViewModel) { return this._viewCells.indexOf(cell as CellViewModel); } + getVersionId() { + return this._model.notebook.versionId; + } + + getTrackedRange(id: string): ICellRange | null { + return this.getDecorationRange(id); + } + + getDecorationRange(decorationId: string): ICellRange | null { + const node = this._decorations[decorationId]; + if (!node) { + return null; + } + const versionId = this.getVersionId(); + if (node.cachedVersionId !== versionId) { + this._decorationsTree.resolveNode(node, versionId); + } + if (node.range === null) { + return { start: node.cachedAbsoluteStart - 1, length: node.cachedAbsoluteEnd - node.cachedAbsoluteStart + 1 }; + } + + return { start: node.range.startLineNumber - 1, length: node.range.endLineNumber - node.range.startLineNumber + 1 }; + } + + setTrackedRange(id: string | null, newRange: ICellRange | null, newStickiness: TrackedRangeStickiness): string | null { + const node = (id ? this._decorations[id] : null); + + if (!node) { + if (!newRange) { + return null; + } + + return this._deltaCellDecorationsImpl(0, [], [{ range: new Range(newRange.start + 1, 1, newRange.start + newRange.length, 1), options: TRACKED_RANGE_OPTIONS[newStickiness] }])[0]; + } + + if (!newRange) { + // node exists, the request is to delete => delete node + this._decorationsTree.delete(node); + delete this._decorations[node.id]; + return null; + } + + this._decorationsTree.delete(node); + node.reset(this.getVersionId(), newRange.start, newRange.start + newRange.length, new Range(newRange.start + 1, 1, newRange.start + newRange.length, 1)); + node.setOptions(TRACKED_RANGE_OPTIONS[newStickiness]); + this._decorationsTree.insert(node); + return node.id; + } + + private _deltaCellDecorationsImpl(ownerId: number, oldDecorationsIds: string[], newDecorations: IModelDeltaDecoration[]): string[] { + const versionId = this.getVersionId(); + + const oldDecorationsLen = oldDecorationsIds.length; + let oldDecorationIndex = 0; + + const newDecorationsLen = newDecorations.length; + let newDecorationIndex = 0; + + let result = new Array(newDecorationsLen); + while (oldDecorationIndex < oldDecorationsLen || newDecorationIndex < newDecorationsLen) { + + let node: IntervalNode | null = null; + + if (oldDecorationIndex < oldDecorationsLen) { + // (1) get ourselves an old node + do { + node = this._decorations[oldDecorationsIds[oldDecorationIndex++]]; + } while (!node && oldDecorationIndex < oldDecorationsLen); + + // (2) remove the node from the tree (if it exists) + if (node) { + this._decorationsTree.delete(node); + // this._onDidChangeDecorations.checkAffectedAndFire(node.options); + } + } + + if (newDecorationIndex < newDecorationsLen) { + // (3) create a new node if necessary + if (!node) { + const internalDecorationId = (++this._lastDecorationId); + const decorationId = `${this._instanceId};${internalDecorationId}`; + node = new IntervalNode(decorationId, 0, 0); + this._decorations[decorationId] = node; + } + + // (4) initialize node + const newDecoration = newDecorations[newDecorationIndex]; + // const range = this._validateRangeRelaxedNoAllocations(newDecoration.range); + const range = newDecoration.range; + const options = _normalizeOptions(newDecoration.options); + // const startOffset = this._buffer.getOffsetAt(range.startLineNumber, range.startColumn); + // const endOffset = this._buffer.getOffsetAt(range.endLineNumber, range.endColumn); + + node.ownerId = ownerId; + node.reset(versionId, range.startLineNumber, range.endLineNumber, Range.lift(range)); + node.setOptions(options); + // this._onDidChangeDecorations.checkAffectedAndFire(options); + + this._decorationsTree.insert(node); + + result[newDecorationIndex] = node.id; + + newDecorationIndex++; + } else { + if (node) { + delete this._decorations[node.id]; + } + } + } + + return result; + } + private _insertCellDelegate(insertIndex: number, insertCell: CellViewModel) { this._viewCells!.splice(insertIndex, 0, insertCell); this._model.insertCell(insertCell.model, insertIndex); @@ -192,6 +380,7 @@ export class NotebookViewModel extends Disposable { deleteCell: this._deleteCellDelegate.bind(this) })); + this._decorationsTree.acceptReplace(index, 0, 1, true); this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 0, [newCell]]] }); return newCell; } @@ -206,6 +395,7 @@ export class NotebookViewModel extends Disposable { deleteCell: this._deleteCellDelegate.bind(this) })); + this._decorationsTree.acceptReplace(index, 0, 1, true); this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 0, [newCell]]] }); return newCell; } @@ -223,6 +413,9 @@ export class NotebookViewModel extends Disposable { } })); + + this._decorationsTree.acceptReplace(index, 1, 0, true); + this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] }); viewCell.dispose(); } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index dd089ca443484dc93b95f47272df4b262481970d..00ad9e71fc61af5829fa5acb6eccb8ad42ecc5cf 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -31,6 +31,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel private _isUntitled: boolean | undefined = undefined; private _versionId = 0; + get versionId() { + return this._versionId; + } + constructor( public handle: number, public viewType: string, diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 1e4b71c1428a8d86645bffd5357e3be55ce69de9..6d93d62a004fabe5ce9d47aee82a07f0222b99c2 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -173,6 +173,7 @@ export interface INotebookTextModel { viewType: string; // metadata: IMetadata; readonly uri: URI; + readonly versionId: number; languages: string[]; cells: ICell[]; renderers: Set; diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 414ed36108cbbbb5decb21dada50b20e571d5b39..2db7b23d1e35a1b6bcd09cc36b89bbe26535c76e 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -8,12 +8,14 @@ import { URI } from 'vs/base/common/uri'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, diff } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook, TestCell } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { TrackedRangeStickiness } from 'vs/editor/common/model'; +import { reduceCellRanges, ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; suite('NotebookViewModel', () => { const instantiationService = new TestInstantiationService(); @@ -45,12 +47,12 @@ suite('NotebookViewModel', () => { const cell = viewModel.insertCell(1, new TestCell(viewModel.viewType, 0, ['var c = 3;'], 'javascript', CellKind.Code, []), true); assert.equal(viewModel.viewCells.length, 3); assert.equal(viewModel.notebookDocument.cells.length, 3); - assert.equal(viewModel.getViewCellIndex(cell), 1); + assert.equal(viewModel.getCellIndex(cell), 1); viewModel.deleteCell(1, true); assert.equal(viewModel.viewCells.length, 2); assert.equal(viewModel.notebookDocument.cells.length, 2); - assert.equal(viewModel.getViewCellIndex(cell), -1); + assert.equal(viewModel.getCellIndex(cell), -1); } ); }); @@ -68,18 +70,18 @@ suite('NotebookViewModel', () => { const firstViewCell = viewModel.viewCells[0]; const lastViewCell = viewModel.viewCells[viewModel.viewCells.length - 1]; - const insertIndex = viewModel.getViewCellIndex(firstViewCell) + 1; + const insertIndex = viewModel.getCellIndex(firstViewCell) + 1; const cell = viewModel.insertCell(insertIndex, new TestCell(viewModel.viewType, 3, ['var c = 3;'], 'javascript', CellKind.Code, []), true); - const addedCellIndex = viewModel.getViewCellIndex(cell); + const addedCellIndex = viewModel.getCellIndex(cell); viewModel.deleteCell(addedCellIndex, true); - const secondInsertIndex = viewModel.getViewCellIndex(lastViewCell) + 1; + const secondInsertIndex = viewModel.getCellIndex(lastViewCell) + 1; const cell2 = viewModel.insertCell(secondInsertIndex, new TestCell(viewModel.viewType, 4, ['var d = 4;'], 'javascript', CellKind.Code, []), true); assert.equal(viewModel.viewCells.length, 3); assert.equal(viewModel.notebookDocument.cells.length, 3); - assert.equal(viewModel.getViewCellIndex(cell2), 2); + assert.equal(viewModel.getCellIndex(cell2), 2); } ); }); @@ -178,3 +180,184 @@ suite('NotebookViewModel', () => { ); }); }); + +function getVisibleCells(cells: any[], hiddenRanges: ICellRange[]) { + if (!hiddenRanges.length) { + return cells; + } + + let start = 0; + let hiddenRangeIndex = 0; + let result: any[] = []; + + while (start < cells.length && hiddenRangeIndex < hiddenRanges.length) { + if (start < hiddenRanges[hiddenRangeIndex].start) { + result.push(...cells.slice(start, hiddenRanges[hiddenRangeIndex].start)); + } + + start = hiddenRanges[hiddenRangeIndex].start + hiddenRanges[hiddenRangeIndex].length; + hiddenRangeIndex++; + } + + if (start < cells.length) { + result.push(...cells.slice(start)); + } + + return result; +} + +suite('NotebookViewModel Decorations', () => { + const instantiationService = new TestInstantiationService(); + const blukEditService = instantiationService.get(IBulkEditService); + const undoRedoService = instantiationService.stub(IUndoRedoService, () => { }); + instantiationService.spy(IUndoRedoService, 'pushElement'); + + test('tracking range', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ], + (editor, viewModel) => { + const trackedId = viewModel.setTrackedRange('test', { start: 1, length: 2 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); + assert.deepEqual(viewModel.getTrackedRange(trackedId!), { + start: 1, + length: 2 + }); + + viewModel.insertCell(0, new TestCell(viewModel.viewType, 5, ['var d = 6;'], 'javascript', CellKind.Code, []), true); + assert.deepEqual(viewModel.getTrackedRange(trackedId!), { + start: 2, + length: 2 + }); + + viewModel.deleteCell(0, true); + assert.deepEqual(viewModel.getTrackedRange(trackedId!), { + start: 1, + length: 2 + }); + + viewModel.insertCell(3, new TestCell(viewModel.viewType, 6, ['var d = 7;'], 'javascript', CellKind.Code, []), true); + assert.deepEqual(viewModel.getTrackedRange(trackedId!), { + start: 1, + length: 3 + }); + + viewModel.deleteCell(3, true); + assert.deepEqual(viewModel.getTrackedRange(trackedId!), { + start: 1, + length: 2 + }); + + viewModel.deleteCell(1, true); + assert.deepEqual(viewModel.getTrackedRange(trackedId!), { + start: 0, + length: 2 + }); + } + ); + }); + + test('tracking range 2', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + [['var e = 6;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + [['var e = 7;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ], + (editor, viewModel) => { + const trackedId = viewModel.setTrackedRange('test', { start: 1, length: 3 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); + assert.deepEqual(viewModel.getTrackedRange(trackedId!), { + start: 1, + length: 3 + }); + + viewModel.insertCell(5, new TestCell(viewModel.viewType, 8, ['var d = 9;'], 'javascript', CellKind.Code, []), true); + assert.deepEqual(viewModel.getTrackedRange(trackedId!), { + start: 1, + length: 3 + }); + + viewModel.insertCell(4, new TestCell(viewModel.viewType, 9, ['var d = 10;'], 'javascript', CellKind.Code, []), true); + assert.deepEqual(viewModel.getTrackedRange(trackedId!), { + start: 1, + length: 4 + }); + } + ); + }); + + test('reduce range', function () { + assert.deepEqual(reduceCellRanges([ + { start: 0, length: 2 }, + { start: 1, length: 2 }, + { start: 4, length: 2 } + ]), [ + { start: 0, length: 3 }, + { start: 4, length: 2 } + ]); + + assert.deepEqual(reduceCellRanges([ + { start: 0, length: 2 }, + { start: 1, length: 2 }, + { start: 3, length: 2 } + ]), [ + { start: 0, length: 5 } + ]); + }); + + test('diff hidden ranges', function () { + assert.deepEqual(getVisibleCells([1, 2, 3, 4, 5], []), [1, 2, 3, 4, 5]); + + assert.deepEqual( + getVisibleCells( + [1, 2, 3, 4, 5], + [{ start: 1, length: 2 }] + ), + [1, 4, 5] + ); + + assert.deepEqual( + getVisibleCells( + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + { start: 1, length: 2 }, + { start: 4, length: 2 } + ] + ), + [1, 4, 7, 8, 9] + ); + + const original = getVisibleCells( + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + { start: 1, length: 2 }, + { start: 4, length: 2 } + ] + ); + + const modified = getVisibleCells( + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + { start: 2, length: 3 } + ] + ); + + assert.deepEqual(diff(original, modified, (a) => { + return original.indexOf(a) >= 0; + }), [{ start: 1, deleteCount: 1, toInsert: [2, 6] }]); + }); +}); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 3494687cd5ce3f14623b4f625d51b9dbccdf7066..43ca6e0265a5fd3d2f7976fc73b52541ed6fa7ff 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -9,7 +9,7 @@ import { CellKind, IOutput, CellUri, NotebookCellMetadata } from 'vs/workbench/c import { NotebookViewModel, IModelDecorationsChangeAccessor, CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { INotebookEditor, NotebookLayoutInfo, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, NotebookLayoutInfo, ICellViewModel, ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; @@ -49,6 +49,11 @@ export class TestNotebookEditor implements INotebookEditor { constructor( ) { } + + setHiddenAreas(_ranges: ICellRange[]): boolean { + throw new Error('Method not implemented.'); + } + getInnerWebview(): Webview | undefined { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts index f32c21065125a1664eefd9c26155071f225d7cd9..406f9191a3007499536e141f1cd76f186218c012 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts @@ -544,6 +544,10 @@ export class TerminalLinkManager extends DisposableStore { return null; } link = this.osPath.join(this._processCwd, link); + } else { + // Remove \\?\ from paths so that they share the same underlying + // uri and don't open multiple tabs for the same file + link = link.replace(/^\\\\\?\\/, ''); } } else { if (!this._processCwd) { diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts index f85fb9616dcb3530be6bb6c15fcda914766e53fe..b712d0dc8fc13e771d9ec006e36f5f77005b6477 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts @@ -254,6 +254,7 @@ suite('Workbench - TerminalLinkHandler', () => { assert.equal(linkHandler.preprocessPath('~/src/file3'), 'C:\\Users\\Me\\src\\file3'); assert.equal(linkHandler.preprocessPath('~\\src\\file4'), 'C:\\Users\\Me\\src\\file4'); assert.equal(linkHandler.preprocessPath('C:\\absolute\\path\\file5'), 'C:\\absolute\\path\\file5'); + assert.equal(linkHandler.preprocessPath('\\\\?\\C:\\absolute\\path\\extended\\file6'), 'C:\\absolute\\path\\extended\\file6'); }); test('Windows - spaces', () => { const linkHandler = new TestTerminalLinkManager(new TestXterm() as any, { diff --git a/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts index 7be9d0e1c6ddbbb2dac03f68416d75767fdfd41c..67e5bcc9e29cb3d73982b7c030a14359a49b72b9 100644 --- a/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts @@ -28,7 +28,7 @@ import { readFileSync, statSync } from 'fs'; import { detectEncodingByBOM } from 'vs/base/test/node/encoding/encoding.test'; import { workbenchInstantiationService, TestNativeTextFileServiceWithEncodingOverrides } from 'vs/workbench/test/electron-browser/workbenchTestServices'; -suite('Files - TextFileService i/o', () => { +suite('Files - TextFileService i/o', function () { const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'textfileservice'); const disposables = new DisposableStore(); @@ -36,6 +36,14 @@ suite('Files - TextFileService i/o', () => { let service: ITextFileService; let testDir: string; + // Given issues such as https://github.com/microsoft/vscode/issues/78602 + // and https://github.com/microsoft/vscode/issues/92334 we see random test + // failures when accessing the native file system. To diagnose further, we + // retry node.js file access tests up to 3 times to rule out any random disk + // issue and increase the timeout. + this.retries(3); + this.timeout(1000 * 10); + setup(async () => { const instantiationService = workbenchInstantiationService(); diff --git a/yarn.lock b/yarn.lock index 4069aa5ee95a58792d35a2075e25e136a213475d..0d730f0f0e91a6f144d6356c0a43e57cb5669c46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8224,10 +8224,10 @@ semver-greatest-satisfied-range@^1.1.0: dependencies: sver-compat "^1.5.0" -semver-umd@^5.5.5: - version "5.5.5" - resolved "https://registry.yarnpkg.com/semver-umd/-/semver-umd-5.5.5.tgz#a2e4280d0e92a2b27695c18811f0e939e144d86f" - integrity sha512-8rUq0nnTzlexpAdYmm8UDYsLkBn0MnBkfrGWPmyDBDDzv71dPOH07szOOaLj/5hO3BYmumYwS+wp3C60zLzh5g== +semver-umd@^5.5.6: + version "5.5.6" + resolved "https://registry.yarnpkg.com/semver-umd/-/semver-umd-5.5.6.tgz#1d185bbd2caec825c564b54907cd09e14083f228" + integrity sha512-6ARYXVi4Y4VO5HfyCjT/6xyykBtJwEXSGQ8ON4UPQSFOjZUDsbAE0J614QcBBsLTTyQMEqvsXN804vAqpydjzw== "semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0: version "5.4.1"