diff --git a/build/.cachesalt b/build/.cachesalt index 339d2d379fa..3f2ee542ad5 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2019-08-30T20:24:23.714Z +2020-10-05T20:24:23.714Z diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index 0733d47bc72..9f66907f0a1 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -66,7 +66,8 @@ steps: artifactName: crash-dump-macos targetPath: .build/crashes displayName: 'Publish Crash Reports' - condition: succeededOrFailed() + continueOnError: true + condition: failed() - task: PublishTestResults@2 displayName: Publish Tests Results diff --git a/build/azure-pipelines/darwin/entitlements.plist b/build/azure-pipelines/darwin/entitlements.plist index 6631ffa6f24..be8b7163da7 100644 --- a/build/azure-pipelines/darwin/entitlements.plist +++ b/build/azure-pipelines/darwin/entitlements.plist @@ -2,5 +2,13 @@ + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + diff --git a/build/azure-pipelines/darwin/helper-entitlements.plist b/build/azure-pipelines/darwin/helper-entitlements.plist new file mode 100644 index 00000000000..123d12a53e9 --- /dev/null +++ b/build/azure-pipelines/darwin/helper-entitlements.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.disable-library-validation + + + diff --git a/build/azure-pipelines/darwin/helper-gpu-entitlements.plist b/build/azure-pipelines/darwin/helper-gpu-entitlements.plist index 4efe1ce508f..777b3abd95e 100644 --- a/build/azure-pipelines/darwin/helper-gpu-entitlements.plist +++ b/build/azure-pipelines/darwin/helper-gpu-entitlements.plist @@ -4,5 +4,7 @@ com.apple.security.cs.allow-jit + com.apple.security.cs.disable-library-validation + diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 32bfd2c83db..48b8d9ed40a 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -157,7 +157,8 @@ steps: artifactName: crash-dump-macos targetPath: .build/crashes displayName: 'Publish Crash Reports' - condition: succeededOrFailed() + continueOnError: true + condition: failed() - script: | set -e @@ -172,6 +173,7 @@ steps: security import $(agent.tempdirectory)/cert.p12 -k $(agent.tempdirectory)/buildagent.keychain -P "$(macos-developer-certificate-key)" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k pwd $(agent.tempdirectory)/buildagent.keychain codesign -s 99FM488X57 --deep --force --options runtime --entitlements build/azure-pipelines/darwin/entitlements.plist "$APP_ROOT"/*.app + codesign -s 99FM488X57 --force --options runtime --entitlements build/azure-pipelines/darwin/helper-entitlements.plist "$APP_FRAMEWORK_PATH/$HELPER_APP_NAME Helper.app" codesign -s 99FM488X57 --force --options runtime --entitlements build/azure-pipelines/darwin/helper-gpu-entitlements.plist "$APP_FRAMEWORK_PATH/$HELPER_APP_NAME Helper (GPU).app" codesign -s 99FM488X57 --force --options runtime --entitlements build/azure-pipelines/darwin/helper-plugin-entitlements.plist "$APP_FRAMEWORK_PATH/$HELPER_APP_NAME Helper (Plugin).app" codesign -s 99FM488X57 --force --options runtime --entitlements build/azure-pipelines/darwin/helper-renderer-entitlements.plist "$APP_FRAMEWORK_PATH/$HELPER_APP_NAME Helper (Renderer).app" @@ -241,6 +243,13 @@ steps: SessionTimeout: 60 displayName: Notarization +- script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin + APP_NAME="`ls $APP_ROOT | head -n 1`" + "$APP_ROOT/$APP_NAME/Contents/Resources/app/bin/code" --version + displayName: Verify start after signing + - script: | set -e VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ @@ -251,6 +260,11 @@ steps: ./build/azure-pipelines/darwin/publish.sh displayName: Publish +- script: | + yarn gulp upload-vscode-configuration + displayName: Upload configuration (for Bing settings search) + continueOnError: true + - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 displayName: 'Component Detection' continueOnError: true diff --git a/build/azure-pipelines/darwin/publish.sh b/build/azure-pipelines/darwin/publish.sh index f0375d2a3c8..fe3e9a59986 100755 --- a/build/azure-pipelines/darwin/publish.sh +++ b/build/azure-pipelines/darwin/publish.sh @@ -22,6 +22,3 @@ node build/azure-pipelines/common/createAsset.js \ # node build/azure-pipelines/common/symbols.js "$VSCODE_MIXIN_PASSWORD" "$VSCODE_HOCKEYAPP_TOKEN" x64 "$VSCODE_HOCKEYAPP_ID_MACOS" # Skip hockey app because build failure. # https://github.com/microsoft/vscode/issues/90491 - -# upload configuration -yarn gulp upload-vscode-configuration diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index fb0a5b2dd3e..fdd4c305cda 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -75,8 +75,9 @@ steps: artifactName: crash-dump-linux targetPath: .build/crashes displayName: 'Publish Crash Reports' - condition: succeededOrFailed() - + continueOnError: true + condition: failed() + - task: PublishTestResults@2 displayName: Publish Tests Results inputs: diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 76428b860f2..f28c896ba83 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -145,7 +145,8 @@ steps: artifactName: crash-dump-linux targetPath: .build/crashes displayName: 'Publish Crash Reports' - condition: succeededOrFailed() + continueOnError: true + condition: failed() - script: | set -e diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 1f665c8b3de..c3db41e80d5 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -76,7 +76,6 @@ steps: set -e yarn generate-github-config displayName: Generate GitHub config - condition: succeeded() env: OSS_GITHUB_ID: "a5d3c261b032765a78de" OSS_GITHUB_SECRET: $(oss-github-client-secret) @@ -94,6 +93,7 @@ steps: VSO_DEV_GITHUB_SECRET: $(vso-dev-github-client-secret) GITHUB_APP_ID: "Iv1.ae51e546bef24ff1" GITHUB_APP_SECRET: $(github-app-client-secret) + condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'), ne(variables['CacheRestored'], 'true')) - script: | set -e diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index 57385d54299..b0a81aeb8c8 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -73,7 +73,8 @@ steps: inputs: artifactName: crash-dump-windows targetPath: .build\crashes - condition: succeededOrFailed() + continueOnError: true + condition: failed() - task: PublishTestResults@2 displayName: Publish Tests Results diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 75dc54e3599..bcd03489df6 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -154,7 +154,8 @@ steps: artifactName: crash-dump-windows-$(VSCODE_ARCH) targetPath: .build\crashes displayName: 'Publish Crash Reports' - condition: succeededOrFailed() + continueOnError: true + condition: failed() - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 inputs: diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index f6106241ced..6cb65c5a166 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -127,6 +127,7 @@ const createESMSourcesAndResourcesTask = task.define('extract-editor-esm', () => const compileEditorESMTask = task.define('compile-editor-esm', () => { const KEEP_PREV_ANALYSIS = false; + const FAIL_ON_PURPOSE = false; console.log(`Launching the TS compiler at ${path.join(__dirname, '../out-editor-esm')}...`); let result; if (process.platform === 'win32') { @@ -142,7 +143,7 @@ const compileEditorESMTask = task.define('compile-editor-esm', () => { console.log(result.stdout.toString()); console.log(result.stderr.toString()); - if (result.status !== 0) { + if (FAIL_ON_PURPOSE || result.status !== 0) { console.log(`The TS Compilation failed, preparing analysis folder...`); const destPath = path.join(__dirname, '../../vscode-monaco-editor-esm-analysis'); const keepPrevAnalysis = (KEEP_PREV_ANALYSIS && fs.existsSync(destPath)); diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js index cd366b9c524..5b1cf0591ec 100644 --- a/build/lib/treeshaking.js +++ b/build/lib/treeshaking.js @@ -420,7 +420,7 @@ function markNodes(languageService, options) { // (they can be the declaration of a module import) continue; } - if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration))) { + if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program, checker, declaration)) { enqueue_black(declaration.name); for (let j = 0; j < declaration.members.length; j++) { const member = declaration.members[j]; @@ -614,6 +614,34 @@ function generateResult(languageService, shakeLevel) { } //#endregion //#region Utils +function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program, checker, declaration) { + if (!program.isSourceFileDefaultLibrary(declaration.getSourceFile()) && declaration.heritageClauses) { + for (const heritageClause of declaration.heritageClauses) { + for (const type of heritageClause.types) { + const symbol = findSymbolFromHeritageType(checker, type); + if (symbol) { + const decl = symbol.valueDeclaration || (symbol.declarations && symbol.declarations[0]); + if (decl && program.isSourceFileDefaultLibrary(decl.getSourceFile())) { + return true; + } + } + } + } + } + return false; +} +function findSymbolFromHeritageType(checker, type) { + if (ts.isExpressionWithTypeArguments(type)) { + return findSymbolFromHeritageType(checker, type.expression); + } + if (ts.isIdentifier(type)) { + return getRealNodeSymbol(checker, type)[0]; + } + if (ts.isPropertyAccessExpression(type)) { + return findSymbolFromHeritageType(checker, type.name); + } + return null; +} /** * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) */ diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index b65475299f5..405336bfcbb 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -536,7 +536,7 @@ function markNodes(languageService: ts.LanguageService, options: ITreeShakingOpt continue; } - if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration))) { + if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program, checker, declaration)) { enqueue_black(declaration.name!); for (let j = 0; j < declaration.members.length; j++) { @@ -752,6 +752,36 @@ function generateResult(languageService: ts.LanguageService, shakeLevel: ShakeLe //#region Utils +function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program: ts.Program, checker: ts.TypeChecker, declaration: ts.ClassDeclaration | ts.InterfaceDeclaration): boolean { + if (!program.isSourceFileDefaultLibrary(declaration.getSourceFile()) && declaration.heritageClauses) { + for (const heritageClause of declaration.heritageClauses) { + for (const type of heritageClause.types) { + const symbol = findSymbolFromHeritageType(checker, type); + if (symbol) { + const decl = symbol.valueDeclaration || (symbol.declarations && symbol.declarations[0]); + if (decl && program.isSourceFileDefaultLibrary(decl.getSourceFile())) { + return true; + } + } + } + } + } + return false; +} + +function findSymbolFromHeritageType(checker: ts.TypeChecker, type: ts.ExpressionWithTypeArguments | ts.Expression | ts.PrivateIdentifier): ts.Symbol | null { + if (ts.isExpressionWithTypeArguments(type)) { + return findSymbolFromHeritageType(checker, type.expression); + } + if (ts.isIdentifier(type)) { + return getRealNodeSymbol(checker, type)[0]; + } + if (ts.isPropertyAccessExpression(type)) { + return findSymbolFromHeritageType(checker, type.name); + } + return null; +} + /** * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) */ diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index 159f6c72234..3980ba7c7fd 100644 --- a/extensions/github-authentication/src/extension.ts +++ b/extensions/github-authentication/src/extension.ts @@ -25,6 +25,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.authentication.registerAuthenticationProvider({ id: 'github', displayName: 'GitHub', + supportsMultipleAccounts: false, onDidChangeSessions: onDidChangeSessions.event, getSessions: () => Promise.resolve(loginService.sessions), login: async (scopeList: string[]) => { diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index 25d3ac86c22..f4b589e0fb8 100644 --- a/extensions/image-preview/src/preview.ts +++ b/extensions/image-preview/src/preview.ts @@ -179,7 +179,7 @@ class Preview extends Disposable { private async render() { if (this._previewState !== PreviewState.Disposed) { - this.webviewEditor.webview.html = await this.getWebiewContents(); + this.webviewEditor.webview.html = await this.getWebviewContents(); } } @@ -203,7 +203,7 @@ class Preview extends Disposable { } } - private async getWebiewContents(): Promise { + private async getWebviewContents(): Promise { const version = Date.now().toString(); const settings = { isMac: process.platform === 'darwin', @@ -249,9 +249,9 @@ class Preview extends Disposable { // Avoid adding cache busting if there is already a query string if (resource.query) { - return webviewEditor.webview.asWebviewUri(resource).toString(true); + return webviewEditor.webview.asWebviewUri(resource).toString(); } - return webviewEditor.webview.asWebviewUri(resource).with({ query: `version=${version}` }).toString(true); + return webviewEditor.webview.asWebviewUri(resource).with({ query: `version=${version}` }).toString(); } private extensionResource(path: string) { diff --git a/extensions/markdown-language-features/media/markdown.css b/extensions/markdown-language-features/media/markdown.css index 5c06f2b082f..bff2bcdb48d 100644 --- a/extensions/markdown-language-features/media/markdown.css +++ b/extensions/markdown-language-features/media/markdown.css @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ html, body { - font-family: var(--vscode-markdown-font-family, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif); + font-family: var(--vscode-markdown-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif); font-size: var(--vscode-markdown-font-size, 14px); padding: 0 26px; line-height: var(--vscode-markdown-line-height, 22px); @@ -157,7 +157,7 @@ blockquote { } code { - font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + font-family: var(--vscode-editor-font-family, Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"); font-size: 1em; line-height: 1.357em; } diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 541cd4e327a..fcefaef0a56 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -210,7 +210,7 @@ }, "markdown.preview.fontFamily": { "type": "string", - "default": "-apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', 'Ubuntu', 'Droid Sans', sans-serif", + "default": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', 'Ubuntu', 'Droid Sans', sans-serif", "description": "%markdown.preview.fontFamily.desc%", "scope": "resource" }, diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index fbbab999519..6cf645a4103 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -1,7 +1,7 @@ { "displayName": "Markdown Language Features", "description": "Provides rich language support for Markdown.", - "markdown.preview.breaks.desc": "Sets how line-breaks are rendered in the markdown preview. Setting it to 'true' creates a
for every newline.", + "markdown.preview.breaks.desc": "Sets how line-breaks are rendered in the markdown preview. Setting it to 'true' creates a
for newlines inside paragraphs.", "markdown.preview.linkify": "Enable or disable conversion of URL-like text to links in the markdown preview.", "markdown.preview.doubleClickToSwitchToEditor.desc": "Double click in the markdown preview to switch to the editor.", "markdown.preview.fontFamily.desc": "Controls the font family used in the markdown preview.", diff --git a/extensions/npm/src/tasks.ts b/extensions/npm/src/tasks.ts index 661796a3756..2f4947de447 100644 --- a/extensions/npm/src/tasks.ts +++ b/extensions/npm/src/tasks.ts @@ -29,6 +29,8 @@ type AutoDetect = 'on' | 'off'; let cachedTasks: Task[] | undefined = undefined; +const INSTALL_SCRIPT = 'install'; + export class NpmTaskProvider implements TaskProvider { constructor() { @@ -52,7 +54,7 @@ export class NpmTaskProvider implements TaskProvider { } else { packageJsonUri = _task.scope.uri.with({ path: _task.scope.uri.path + '/package.json' }); } - return createTask(kind, `run ${kind.script}`, _task.scope, packageJsonUri); + return createTask(kind, `${kind.script === INSTALL_SCRIPT ? '' : 'run '}${kind.script}`, _task.scope, packageJsonUri); } return undefined; } @@ -253,7 +255,7 @@ async function provideNpmScriptsForFolder(packageJsonUri: Uri): Promise result.push(task); }); // always add npm install (without a problem matcher) - result.push(createTask('install', 'install', folder, packageJsonUri, 'install dependencies from package', [])); + result.push(createTask(INSTALL_SCRIPT, INSTALL_SCRIPT, folder, packageJsonUri, 'install dependencies from package', [])); return result; } diff --git a/extensions/package.json b/extensions/package.json index 343e3053756..8744f4dff90 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.9.1-rc" + "typescript": "^3.9.2-insiders.20200509" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/perl/cgmanifest.json b/extensions/perl/cgmanifest.json index b7c850dd1aa..83d91107671 100644 --- a/extensions/perl/cgmanifest.json +++ b/extensions/perl/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "textmate/perl.tmbundle", "repositoryUrl": "https://github.com/textmate/perl.tmbundle", - "commitHash": "d9841a0878239fa43f88c640f8d458590f97e8f5" + "commitHash": "80826abe75250286c2a1a07958e50e8551d3f50c" } }, "licenseDetail": [ diff --git a/extensions/python/cgmanifest.json b/extensions/python/cgmanifest.json index 97a9d1d1118..37a21b2de54 100644 --- a/extensions/python/cgmanifest.json +++ b/extensions/python/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "MagicStack/MagicPython", "repositoryUrl": "https://github.com/MagicStack/MagicPython", - "commitHash": "0b09c1fca238d22e15ac5712d03f9bf6da626f9c" + "commitHash": "c9b3409deb69acec31bbf7913830e93a046b30cc" } }, "license": "MIT", diff --git a/extensions/python/syntaxes/MagicPython.tmLanguage.json b/extensions/python/syntaxes/MagicPython.tmLanguage.json index a6920a06305..eb3c96990b9 100644 --- a/extensions/python/syntaxes/MagicPython.tmLanguage.json +++ b/extensions/python/syntaxes/MagicPython.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/MagicStack/MagicPython/commit/0b09c1fca238d22e15ac5712d03f9bf6da626f9c", + "version": "https://github.com/MagicStack/MagicPython/commit/2ca894f270f92e2bc8f09a2ebdcd482fbb3b1074", "name": "MagicPython", "scopeName": "source.python", "patterns": [ @@ -31,6 +31,9 @@ { "include": "#function-declaration" }, + { + "include": "#generator" + }, { "include": "#statement-keyword" }, @@ -291,6 +294,9 @@ { "include": "#lambda" }, + { + "include": "#generator" + }, { "include": "#illegal-operator" }, @@ -306,6 +312,9 @@ { "include": "#list" }, + { + "include": "#odd-function-call" + }, { "include": "#round-braces" }, @@ -388,6 +397,9 @@ }, { "include": "#member-access-base" + }, + { + "include": "#member-access-attribute" } ] }, @@ -413,6 +425,11 @@ } ] }, + "member-access-attribute": { + "comment": "Highlight attribute access in otherwise non-specialized cases.", + "name": "meta.attribute.python", + "match": "(?x)\n \\b ([[:alpha:]_]\\w*) \\b\n" + }, "special-names": { "name": "constant.other.caps.python", "match": "(?x)\n \\b\n # we want to see \"enough\", meaning 2 or more upper-case\n # letters in the beginning of the constant\n #\n # for more details refer to:\n # https://github.com/MagicStack/MagicPython/issues/42\n (\n _* [[:upper:]] [_\\d]* [[:upper:]]\n )\n [[:upper:]\\d]* (_\\w*)?\n \\b\n" @@ -459,6 +476,21 @@ } ] }, + "odd-function-call": { + "comment": "A bit obscured function call where there may have been an\narbitrary number of other operations to get the function.\nE.g. \"arr[idx](args)\"\n", + "begin": "(?x)\n (?<= \\] | \\) ) \\s*\n (?=\\()\n", + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + } + }, + "patterns": [ + { + "include": "#function-arguments" + } + ] + }, "round-braces": { "begin": "\\(", "end": "\\)", @@ -1195,6 +1227,26 @@ } ] }, + "generator": { + "comment": "Match \"for ... in\" construct used in generators and for loops to\ncorrectly identify the \"in\" as a control flow keyword.\n", + "begin": "for", + "beginCaptures": { + "0": { + "name": "keyword.control.flow.python" + } + }, + "end": "in", + "endCaptures": { + "0": { + "name": "keyword.control.flow.python" + } + }, + "patterns": [ + { + "include": "#expression" + } + ] + }, "function-declaration": { "name": "meta.function.python", "begin": "(?x)\n \\s*\n (?:\\b(async) \\s+)? \\b(def)\\s+\n (?=\n [[:alpha:]_][[:word:]]* \\s* \\(\n )\n", @@ -1407,6 +1459,7 @@ "include": "#special-names" }, { + "name": "meta.indexed-name.python", "match": "(?x)\n \\b ([[:alpha:]_]\\w*) \\b\n" } ] @@ -1524,6 +1577,7 @@ }, "function-call": { "name": "meta.function-call.python", + "comment": "Regular function call of the type \"name(args)\"", "begin": "(?x)\n \\b(?=\n ([[:alpha:]_]\\w*) \\s* (\\()\n )\n", "end": "(\\))", "endCaptures": { diff --git a/extensions/python/test/colorize-results/test-freeze-56377_py.json b/extensions/python/test/colorize-results/test-freeze-56377_py.json index 63889e26fee..5038c289809 100644 --- a/extensions/python/test/colorize-results/test-freeze-56377_py.json +++ b/extensions/python/test/colorize-results/test-freeze-56377_py.json @@ -254,13 +254,13 @@ }, { "c": "in", - "t": "source.python keyword.operator.logical.python", + "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.operator.logical.python: #569CD6", - "light_plus": "keyword.operator.logical.python: #0000FF", - "dark_vs": "keyword.operator.logical.python: #569CD6", - "light_vs": "keyword.operator.logical.python: #0000FF", - "hc_black": "keyword.operator.logical.python: #569CD6" + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0" } }, { @@ -298,7 +298,7 @@ }, { "c": "request", - "t": "source.python meta.member.access.python", + "t": "source.python meta.member.access.python meta.attribute.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/python/test/colorize-results/test_py.json b/extensions/python/test/colorize-results/test_py.json index c61d2db7611..c0a14453646 100644 --- a/extensions/python/test/colorize-results/test_py.json +++ b/extensions/python/test/colorize-results/test_py.json @@ -430,7 +430,7 @@ }, { "c": "size", - "t": "source.python meta.member.access.python", + "t": "source.python meta.member.access.python meta.attribute.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -914,13 +914,13 @@ }, { "c": "in", - "t": "source.python meta.function.python meta.function.parameters.python keyword.operator.logical.python", + "t": "source.python meta.function.python meta.function.parameters.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.operator.logical.python: #569CD6", - "light_plus": "keyword.operator.logical.python: #0000FF", - "dark_vs": "keyword.operator.logical.python: #569CD6", - "light_vs": "keyword.operator.logical.python: #0000FF", - "hc_black": "keyword.operator.logical.python: #569CD6" + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0" } }, { @@ -2113,13 +2113,13 @@ }, { "c": "in", - "t": "source.python keyword.operator.logical.python", + "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.operator.logical.python: #569CD6", - "light_plus": "keyword.operator.logical.python: #0000FF", - "dark_vs": "keyword.operator.logical.python: #569CD6", - "light_vs": "keyword.operator.logical.python: #0000FF", - "hc_black": "keyword.operator.logical.python: #569CD6" + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0" } }, { @@ -4544,7 +4544,7 @@ }, { "c": "fn", - "t": "source.python meta.member.access.python", + "t": "source.python meta.member.access.python meta.attribute.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4621,7 +4621,7 @@ }, { "c": "memo", - "t": "source.python meta.member.access.python", + "t": "source.python meta.member.access.python meta.attribute.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4918,7 +4918,7 @@ }, { "c": "memo", - "t": "source.python meta.member.access.python", + "t": "source.python meta.member.access.python meta.attribute.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4973,7 +4973,7 @@ }, { "c": "memo", - "t": "source.python meta.member.access.python meta.item-access.python", + "t": "source.python meta.member.access.python meta.item-access.python meta.indexed-name.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5182,7 +5182,7 @@ }, { "c": "memo", - "t": "source.python meta.member.access.python meta.item-access.python", + "t": "source.python meta.member.access.python meta.item-access.python meta.indexed-name.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6797,4 +6797,4 @@ "hc_black": "string: #CE9178" } } -] +] \ No newline at end of file diff --git a/extensions/r/cgmanifest.json b/extensions/r/cgmanifest.json index 7cf785d7140..62f2751e6c9 100644 --- a/extensions/r/cgmanifest.json +++ b/extensions/r/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "Ikuyadeu/vscode-R", "repositoryUrl": "https://github.com/Ikuyadeu/vscode-R", - "commitHash": "bc79e9245682ee09b4f0b742b927a37702d91b82" + "commitHash": "e03ba9cb9b19412f48c73ea73deb6746d50bbf23" } }, "license": "MIT", - "version": "1.1.8" + "version": "1.3.0" } ], "version": 1 diff --git a/extensions/r/syntaxes/r.tmLanguage.json b/extensions/r/syntaxes/r.tmLanguage.json index 2b6c4368da9..ad947f62849 100644 --- a/extensions/r/syntaxes/r.tmLanguage.json +++ b/extensions/r/syntaxes/r.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Ikuyadeu/vscode-R/commit/bc79e9245682ee09b4f0b742b927a37702d91b82", + "version": "https://github.com/Ikuyadeu/vscode-R/commit/e03ba9cb9b19412f48c73ea73deb6746d50bbf23", "name": "R", "scopeName": "source.r", "patterns": [ @@ -168,20 +168,12 @@ "match": "(\\-|\\+|\\*|\\/|%\\/%|%%|%\\*%|%o%|%x%|\\^)", "name": "keyword.operator.arithmetic.r" }, - { - "match": "<=|>=", - "name": "keyword.operator.comparison.r" - }, - { - "match": "==", - "name": "keyword.operator.comarison.r" - }, { "match": "(:=|<-|<<-|->|->>)", "name": "keyword.operator.assignment.r" }, { - "match": "(!=|<>|<|>|%in%)", + "match": "(==|<=|>=|!=|<>|<|>|%in%)", "name": "keyword.operator.comparison.r" }, { diff --git a/extensions/search-result/package.json b/extensions/search-result/package.json index 6a55968bda1..ffb5321ae4d 100644 --- a/extensions/search-result/package.json +++ b/extensions/search-result/package.json @@ -43,8 +43,5 @@ "path": "./syntaxes/searchResult.tmLanguage.json" } ] - }, - "devDependencies": { - "vscode": "^1.1.36" } } diff --git a/extensions/search-result/yarn.lock b/extensions/search-result/yarn.lock index 4ff83ab98bb..fb57ccd13af 100644 --- a/extensions/search-result/yarn.lock +++ b/extensions/search-result/yarn.lock @@ -2,601 +2,3 @@ # yarn lockfile v1 -agent-base@4, agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== - dependencies: - es6-promisify "^5.0.0" - -ajv@^6.5.5: - version "6.10.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" - integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -browser-stdout@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" - integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -debug@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -diff@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= - dependencies: - es6-promise "^4.0.3" - -escape-string-regexp@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -glob@7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.2: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== - dependencies: - ajv "^6.5.5" - har-schema "^2.0.0" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -he@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= - -http-proxy-agent@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" - integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== - dependencies: - agent-base "4" - debug "3.1.0" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -https-proxy-agent@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" - integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -mime-db@1.40.0: - version "1.40.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" - integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.24" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" - integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== - dependencies: - mime-db "1.40.0" - -minimatch@3.0.4, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -mkdirp@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -mocha@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" - integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== - dependencies: - browser-stdout "1.3.1" - commander "2.15.1" - debug "3.1.0" - diff "3.5.0" - escape-string-regexp "1.0.5" - glob "7.1.2" - growl "1.10.5" - he "1.1.1" - minimatch "3.0.4" - mkdirp "0.5.1" - supports-color "5.4.0" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -psl@^1.1.24: - version "1.4.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" - integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -querystringify@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" - integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== - -request@^2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - -safe-buffer@^5.0.1, safe-buffer@^5.1.2: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== - -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -semver@^5.4.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -source-map-support@^0.5.0: - version "0.5.16" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -supports-color@5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" - integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w== - dependencies: - has-flag "^3.0.0" - -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -url-parse@^1.4.4: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -uuid@^3.3.2: - version "3.3.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" - integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -vscode-test@^0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/vscode-test/-/vscode-test-0.4.3.tgz#461ebf25fc4bc93d77d982aed556658a2e2b90b8" - integrity sha512-EkMGqBSefZH2MgW65nY05rdRSko15uvzq4VAPM5jVmwYuFQKE7eikKXNJDRxL+OITXHB6pI+a3XqqD32Y3KC5w== - dependencies: - http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.1" - -vscode@^1.1.36: - version "1.1.36" - resolved "https://registry.yarnpkg.com/vscode/-/vscode-1.1.36.tgz#5e1a0d1bf4977d0c7bc5159a9a13d5b104d4b1b6" - integrity sha512-cGFh9jmGLcTapCpPCKvn8aG/j9zVQ+0x5hzYJq5h5YyUXVGa1iamOaB2M2PZXoumQPES4qeAP1FwkI0b6tL4bQ== - dependencies: - glob "^7.1.2" - mocha "^5.2.0" - request "^2.88.0" - semver "^5.4.1" - source-map-support "^0.5.0" - url-parse "^1.4.4" - vscode-test "^0.4.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/extensions/sql/cgmanifest.json b/extensions/sql/cgmanifest.json index 859d6782240..56b576cdc79 100644 --- a/extensions/sql/cgmanifest.json +++ b/extensions/sql/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "Microsoft/vscode-mssql", "repositoryUrl": "https://github.com/Microsoft/vscode-mssql", - "commitHash": "a542fe96780e6b274adb281810d419a512fb5bb4" + "commitHash": "37a22725186b5b481b2882a78c7b9fe024c13946" } }, "license": "MIT", diff --git a/extensions/sql/syntaxes/sql.tmLanguage.json b/extensions/sql/syntaxes/sql.tmLanguage.json index d4173da5b23..778c35071fa 100644 --- a/extensions/sql/syntaxes/sql.tmLanguage.json +++ b/extensions/sql/syntaxes/sql.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/vscode-mssql/commit/a542fe96780e6b274adb281810d419a512fb5bb4", + "version": "https://github.com/Microsoft/vscode-mssql/commit/37a22725186b5b481b2882a78c7b9fe024c13946", "name": "SQL", "scopeName": "source.sql", "patterns": [ diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/features/completions.ts index df7f7ea9a5b..97051395de9 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/features/completions.ts @@ -380,7 +380,6 @@ interface CompletionConfiguration { readonly nameSuggestions: boolean; readonly pathSuggestions: boolean; readonly autoImportSuggestions: boolean; - readonly includeAutomaticOptionalChainCompletions: boolean; } namespace CompletionConfiguration { @@ -388,7 +387,6 @@ namespace CompletionConfiguration { export const nameSuggestions = 'suggest.names'; export const pathSuggestions = 'suggest.paths'; export const autoImportSuggestions = 'suggest.autoImports'; - export const includeAutomaticOptionalChainCompletions = 'suggest.includeAutomaticOptionalChainCompletions'; export function getConfigurationForResource( modeId: string, @@ -400,12 +398,11 @@ namespace CompletionConfiguration { pathSuggestions: config.get(CompletionConfiguration.pathSuggestions, true), autoImportSuggestions: config.get(CompletionConfiguration.autoImportSuggestions, true), nameSuggestions: config.get(CompletionConfiguration.nameSuggestions, true), - includeAutomaticOptionalChainCompletions: config.get(CompletionConfiguration.includeAutomaticOptionalChainCompletions, true), }; } } -class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider { +class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider { public static readonly triggerCharacters = ['.', '"', '\'', '`', '/', '@', '<', '#']; @@ -428,9 +425,9 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext - ): Promise { + ): Promise | null> { if (this.typingsStatus.isAcquiringTypings) { - return Promise.reject({ + return Promise.reject>({ label: localize( { key: 'acquiringTypingsLabel', comment: ['Typings refers to the *.d.ts typings files that power our IntelliSense. It should not be localized'] }, 'Acquiring typings...'), @@ -456,12 +453,11 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider await this.client.interruptGetErr(() => this.fileConfigurationManager.ensureConfigurationForDocument(document, token)); - const args: Proto.CompletionsRequestArgs & { includeAutomaticOptionalChainCompletions?: boolean } = { + const args: Proto.CompletionsRequestArgs = { ...typeConverters.Position.toFileLocationRequestArgs(file, position), includeExternalModuleExports: completionConfiguration.autoImportSuggestions, includeInsertTextCompletions: true, triggerCharacter: this.getTsTriggerCharacter(context), - includeAutomaticOptionalChainCompletions: completionConfiguration.includeAutomaticOptionalChainCompletions, }; let isNewIdentifierLocation = true; @@ -535,7 +531,7 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider useFuzzyWordRangeLogic: this.client.apiVersion.lt(API.v390), }; - const items: vscode.CompletionItem[] = []; + const items: MyCompletionItem[] = []; for (let entry of entries) { if (!shouldExcludeCompletionEntry(entry, completionConfiguration)) { items.push(new MyCompletionItem(position, document, entry, completionContext, metadata)); @@ -565,13 +561,9 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider } public async resolveCompletionItem( - item: vscode.CompletionItem, + item: MyCompletionItem, token: vscode.CancellationToken - ): Promise { - if (!(item instanceof MyCompletionItem)) { - return undefined; - } - + ): Promise { const filepath = this.client.toOpenedFilePath(item.document); if (!filepath) { return undefined; diff --git a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts index 686c35e18ed..0d54ccea22a 100644 --- a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts @@ -10,29 +10,16 @@ import API from '../utils/api'; import { Disposable } from '../utils/dispose'; import * as fileSchemes from '../utils/fileSchemes'; import { isTypeScriptDocument } from '../utils/languageModeIds'; +import { equals } from '../utils/objects'; import { ResourceMap } from '../utils/resourceMap'; - -function objsAreEqual(a: T, b: T): boolean { - let keys = Object.keys(a); - for (const key of keys) { - if ((a as any)[key] !== (b as any)[key]) { - return false; - } - } - return true; -} - interface FileConfiguration { readonly formatOptions: Proto.FormatCodeSettings; readonly preferences: Proto.UserPreferences; } function areFileConfigurationsEqual(a: FileConfiguration, b: FileConfiguration): boolean { - return ( - objsAreEqual(a.formatOptions, b.formatOptions) - && objsAreEqual(a.preferences, b.preferences) - ); + return equals(a, b); } export default class FileConfigurationManager extends Disposable { @@ -176,16 +163,21 @@ export default class FileConfigurationManager extends Disposable { } const config = vscode.workspace.getConfiguration( + isTypeScriptDocument(document) ? 'typescript' : 'javascript', + document.uri); + + const preferencesConfig = vscode.workspace.getConfiguration( isTypeScriptDocument(document) ? 'typescript.preferences' : 'javascript.preferences', document.uri); const preferences: Proto.UserPreferences = { - quotePreference: this.getQuoteStylePreference(config), - importModuleSpecifierPreference: getImportModuleSpecifierPreference(config), - importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(config), + quotePreference: this.getQuoteStylePreference(preferencesConfig), + importModuleSpecifierPreference: getImportModuleSpecifierPreference(preferencesConfig), + importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(preferencesConfig), allowTextChangesInNewFiles: document.uri.scheme === fileSchemes.file, - providePrefixAndSuffixTextForRename: config.get('renameShorthandProperties', true) === false ? false : config.get('useAliasesForRenames', true), + providePrefixAndSuffixTextForRename: preferencesConfig.get('renameShorthandProperties', true) === false ? false : preferencesConfig.get('useAliasesForRenames', true), allowRenameOfImportPath: true, + includeAutomaticOptionalChainCompletions: config.get('suggest.includeAutomaticOptionalChainCompletions', true), }; return preferences; diff --git a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index 1e0d59f1057..0be49c3113b 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -18,6 +18,7 @@ import TypeScriptServiceClient from './typescriptServiceClient'; import { coalesce, flatten } from './utils/arrays'; import { CommandManager } from './utils/commandManager'; import { Disposable } from './utils/dispose'; +import * as errorCodes from './utils/errorCodes'; import { DiagnosticLanguage, LanguageDescription } from './utils/languageDescription'; import LogDirectoryProvider from './utils/logDirectoryProvider'; import { PluginManager } from './utils/plugins'; @@ -27,13 +28,13 @@ import VersionStatus from './utils/versionStatus'; // Style check diagnostics that can be reported as warnings const styleCheckDiagnostics = [ - 6133, // variable is declared but never used - 6138, // property is declared but its value is never read - 6192, // All imports are unused - 7027, // unreachable code detected - 7028, // unused label - 7029, // fall through case in switch - 7030 // not all code paths return a value + errorCodes.variableDeclaredButNeverUsed, + errorCodes.propertyDeclaretedButNeverUsed, + errorCodes.allImportsAreUnused, + errorCodes.unreachableCode, + errorCodes.unusedLabel, + errorCodes.fallThroughCaseInSwitch, + errorCodes.notAllCodePathsReturnAValue, ]; export default class TypeScriptServiceClientHost extends Disposable { @@ -97,23 +98,31 @@ export default class TypeScriptServiceClientHost extends Disposable { this.client.onReady(() => { const languages = new Set(); for (const plugin of pluginManager.plugins) { - for (const language of plugin.languages) { - languages.add(language); + if (plugin.configNamespace && plugin.languages.length) { + this.registerExtensionLanguageProvider({ + id: plugin.configNamespace, + modeIds: Array.from(plugin.languages), + diagnosticSource: 'ts-plugin', + diagnosticLanguage: DiagnosticLanguage.TypeScript, + diagnosticOwner: 'typescript', + isExternal: true + }, onCompletionAccepted); + } else { + for (const language of plugin.languages) { + languages.add(language); + } } } + if (languages.size) { - const description: LanguageDescription = { + this.registerExtensionLanguageProvider({ id: 'typescript-plugins', modeIds: Array.from(languages.values()), diagnosticSource: 'ts-plugin', diagnosticLanguage: DiagnosticLanguage.TypeScript, diagnosticOwner: 'typescript', isExternal: true - }; - const manager = new LanguageProvider(this.client, description, this.commandManager, this.client.telemetryReporter, this.typingsStatus, this.fileConfigurationManager, onCompletionAccepted); - this.languages.push(manager); - this._register(manager); - this.languagePerId.set(description.id, manager); + }, onCompletionAccepted); } }); @@ -125,6 +134,13 @@ export default class TypeScriptServiceClientHost extends Disposable { this.configurationChanged(); } + private registerExtensionLanguageProvider(description: LanguageDescription, onCompletionAccepted: (item: vscode.CompletionItem) => void) { + const manager = new LanguageProvider(this.client, description, this.commandManager, this.client.telemetryReporter, this.typingsStatus, this.fileConfigurationManager, onCompletionAccepted); + this.languages.push(manager); + this._register(manager); + this.languagePerId.set(description.id, manager); + } + private getAllModeIds(descriptions: LanguageDescription[], pluginManager: PluginManager) { const allModeIds = flatten([ ...descriptions.map(x => x.modeIds), diff --git a/extensions/typescript-language-features/src/utils/errorCodes.ts b/extensions/typescript-language-features/src/utils/errorCodes.ts new file mode 100644 index 00000000000..4407b3a054b --- /dev/null +++ b/extensions/typescript-language-features/src/utils/errorCodes.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const variableDeclaredButNeverUsed = 6133; +export const propertyDeclaretedButNeverUsed = 6138; +export const allImportsAreUnused = 6192; +export const unreachableCode = 7027; +export const unusedLabel = 7028; +export const fallThroughCaseInSwitch = 7029; +export const notAllCodePathsReturnAValue = 7030; diff --git a/extensions/typescript-language-features/src/utils/plugins.ts b/extensions/typescript-language-features/src/utils/plugins.ts index 71ce2430cb8..6d0708b15db 100644 --- a/extensions/typescript-language-features/src/utils/plugins.ts +++ b/extensions/typescript-language-features/src/utils/plugins.ts @@ -12,6 +12,7 @@ export interface TypeScriptServerPlugin { readonly name: string; readonly enableForWorkspaceTypeScriptVersions: boolean; readonly languages: ReadonlyArray; + readonly configNamespace?: string } namespace TypeScriptServerPlugin { @@ -77,6 +78,7 @@ export class PluginManager extends Disposable { enableForWorkspaceTypeScriptVersions: !!plugin.enableForWorkspaceTypeScriptVersions, path: extension.extensionPath, languages: Array.isArray(plugin.languages) ? plugin.languages : [], + configNamespace: plugin.configNamespace, }); } if (plugins.length) { @@ -86,4 +88,4 @@ export class PluginManager extends Disposable { } return pluginMap; } -} \ No newline at end of file +} diff --git a/extensions/vscode-account/package.json b/extensions/vscode-account/package.json index 0deacbea46b..6466cc59663 100644 --- a/extensions/vscode-account/package.json +++ b/extensions/vscode-account/package.json @@ -16,21 +16,6 @@ ], "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "main": "./out/extension.js", - "contributes": { - "configuration": { - "title": "Microsoft Account", - "properties": { - "microsoftAccount.logLevel": { - "type": "string", - "enum": [ - "info", - "trace" - ], - "default": "info" - } - } - } - }, "scripts": { "vscode:prepublish": "npm run compile", "compile": "gulp compile-extension:vscode-account", diff --git a/extensions/vscode-account/src/extension.ts b/extensions/vscode-account/src/extension.ts index 0f0b33b96ec..cc2954e05a6 100644 --- a/extensions/vscode-account/src/extension.ts +++ b/extensions/vscode-account/src/extension.ts @@ -20,6 +20,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.authentication.registerAuthenticationProvider({ id: 'microsoft', displayName: 'Microsoft', + supportsMultipleAccounts: true, onDidChangeSessions: onDidChangeSessions.event, getSessions: () => Promise.resolve(loginService.sessions), login: async (scopes: string[]) => { diff --git a/extensions/vscode-account/src/keychain.ts b/extensions/vscode-account/src/keychain.ts index eb9f54acab3..afda7035521 100644 --- a/extensions/vscode-account/src/keychain.ts +++ b/extensions/vscode-account/src/keychain.ts @@ -62,7 +62,6 @@ export class Keychain { async setToken(token: string): Promise { try { - Logger.trace('Writing to keychain', token); return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token); } catch (e) { Logger.error(`Setting token failed: ${e}`); @@ -85,9 +84,7 @@ export class Keychain { async getToken(): Promise { try { - const result = await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID); - Logger.trace('Reading from keychain', result); - return result; + return await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID); } catch (e) { // Ignore Logger.error(`Getting token failed: ${e}`); diff --git a/extensions/vscode-account/src/logger.ts b/extensions/vscode-account/src/logger.ts index d982ee69f0c..b9ed27bd993 100644 --- a/extensions/vscode-account/src/logger.ts +++ b/extensions/vscode-account/src/logger.ts @@ -5,25 +5,13 @@ import * as vscode from 'vscode'; -type LogLevel = 'Trace' | 'Info' | 'Error'; - -enum Level { - Trace = 'trace', - Info = 'Info' -} +type LogLevel = 'Info' | 'Error'; class Log { private output: vscode.OutputChannel; - private level: Level; constructor() { this.output = vscode.window.createOutputChannel('Microsoft Authentication'); - this.level = vscode.workspace.getConfiguration('microsoftAccount').get('logLevel') || Level.Info; - vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('microsoftAccount.logLevel')) { - this.level = vscode.workspace.getConfiguration('microsoftAccount').get('logLevel') || Level.Info; - } - }); } private data2String(data: any): string { @@ -44,12 +32,6 @@ class Log { this.logLevel('Error', message, data); } - public trace(message: string, data?: any): void { - if (this.level === Level.Trace) { - this.logLevel('Trace', message, data); - } - } - public logLevel(level: LogLevel, message: string, data?: any): void { this.output.appendLine(`[${level} - ${this.now()}] ${message}`); if (data) { diff --git a/extensions/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-notebook-tests/src/notebook.test.ts index e4eba9aefb3..0de2917cf3f 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-notebook-tests/src/notebook.test.ts @@ -212,7 +212,7 @@ suite('notebook dirty state', () => { await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); - // await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.files.newUntitledFile'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); @@ -272,3 +272,51 @@ suite('notebook undo redo', () => { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }); }); + +suite('notebook working copy', () => { + test('notebook revert on close', async function () { + const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + await waitFor(500); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); + + await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); + await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + + // close active editor from command will revert the file + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + await waitFor(500); + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true); + assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[0], vscode.notebook.activeNotebookEditor?.selection); + assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, 'test'); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); + + test('notebook revert', async function () { + const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + await waitFor(500); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); + + await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); + await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + await vscode.commands.executeCommand('workbench.action.files.revert'); + + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true); + assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[0], vscode.notebook.activeNotebookEditor?.selection); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells.length, 1); + assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, 'test'); + + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); +}); diff --git a/extensions/vscode-notebook-tests/src/notebookTestMain.ts b/extensions/vscode-notebook-tests/src/notebookTestMain.ts index b27f6760475..4281ba002fd 100644 --- a/extensions/vscode-notebook-tests/src/notebookTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookTestMain.ts @@ -6,12 +6,22 @@ import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext): any { - context.subscriptions.push(vscode.notebook.registerNotebookProvider('notebookCoreTest', { - resolveNotebook: async (editor: vscode.NotebookEditor) => { - await editor.edit(eb => { - eb.insert(0, 'test', 'typescript', vscode.CellKind.Code, [], {}); - }); - return; + context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { + onDidChangeNotebook: new vscode.EventEmitter().event, + openNotebook: async (_resource: vscode.Uri) => { + return { + languages: ['typescript'], + metadata: {}, + cells: [ + { + source: 'test', + language: 'typescript', + cellKind: vscode.CellKind.Code, + outputs: [], + metadata: {} + } + ] + }; }, executeCell: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => { if (!_cell) { @@ -26,8 +36,11 @@ export function activate(context: vscode.ExtensionContext): any { }]; return; }, - save: async (_document: vscode.NotebookDocument) => { - return true; + saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; + }, + saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; } })); } diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 0a5236436a5..4e0ca82acd1 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@3.9.1-rc: - version "3.9.1-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.1-rc.tgz#81d5a5a0a597e224b6e2af8dffb46524b2eaf5f3" - integrity sha512-+cPv8L2Vd4KidCotqi2wjegBZ5n47CDRUu/QiLVu2YbeXAz78hIfcai9ziBiNI6JTGTVwUqXRug2UZxDcxhvFw== +typescript@^3.9.2-insiders.20200509: + version "3.9.2-insiders.20200509" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2-insiders.20200509.tgz#8c90ed86a91f9692f10f5ac9c1fd6cb241419e6c" + integrity sha512-AAbhs55BZMbyHGfJd0pNfO3+B6jjPpa38zgaIb9MRExkRGLkIUpbUetoh+HgmM5LAtg128sHGiwhLc49pOcgFw== diff --git a/package.json b/package.json index 4b4ea6e65d7..61e48f8384f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.46.0", - "distro": "ce946b3269a7188e873f31dd642911089c3e03e7", + "distro": "6ec59b3610ac45e47ab420d15a0588817178a043", "author": { "name": "Microsoft Corporation" }, @@ -147,7 +147,7 @@ "opn": "^6.0.0", "optimist": "0.3.5", "p-all": "^1.0.0", - "playwright": "0.15.0", + "playwright": "1.0.1", "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", diff --git a/src/main.js b/src/main.js index 9309c12a747..23915126b8f 100644 --- a/src/main.js +++ b/src/main.js @@ -35,6 +35,9 @@ app.setPath('userData', userDataPath); // Set temp directory based on crash-reporter-directory CLI argument // The crash reporter will store crashes in temp folder so we need // to change that location accordingly. + +// If a crash-reporter-directory is specified we setup the crash reporter +// right from the beginning as early as possible to monitor all processes. let crashReporterDirectory = args['crash-reporter-directory']; if (crashReporterDirectory) { crashReporterDirectory = path.normalize(crashReporterDirectory); @@ -47,8 +50,20 @@ if (crashReporterDirectory) { app.exit(1); } } + + // Crashes are stored in the temp directory by default, so we + // need to change that directory to the provided one console.log(`Found --crash-reporter-directory argument. Setting temp directory to be '${crashReporterDirectory}'`); app.setPath('temp', crashReporterDirectory); + + // Start crash reporter + const { crashReporter } = require('electron'); + crashReporter.start({ + companyName: 'Microsoft', + productName: product.nameShort, + submitURL: '', + uploadToServer: false + }); } // Set logs path before app 'ready' event if running portable diff --git a/src/vs/base/browser/ui/countBadge/countBadge.css b/src/vs/base/browser/ui/countBadge/countBadge.css index db90e7ae552..ce26af5e6a1 100644 --- a/src/vs/base/browser/ui/countBadge/countBadge.css +++ b/src/vs/base/browser/ui/countBadge/countBadge.css @@ -15,3 +15,8 @@ display: inline-block; box-sizing: border-box; } + +.monaco-count-badge.long { + padding: 2px 3px; + border-radius: 2px; +} diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 5f17647363c..99c11cdd6e2 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -14,6 +14,7 @@ import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes'; import { EventHelper, EventType, removeClass, addClass, append, $, addDisposableListener, addClasses } from 'vs/base/browser/dom'; import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Emitter } from 'vs/base/common/event'; export interface ILabelRenderer { (container: HTMLElement): IDisposable | null; @@ -29,7 +30,10 @@ export class BaseDropdown extends ActionRunner { private boxContainer?: HTMLElement; private _label?: HTMLElement; private contents?: HTMLElement; + private visible: boolean | undefined; + private _onDidChangeVisibility = new Emitter(); + readonly onDidChangeVisibility = this._onDidChangeVisibility.event; constructor(container: HTMLElement, options: IBaseDropdownOptions) { super(); @@ -101,11 +105,17 @@ export class BaseDropdown extends ActionRunner { } show(): void { - this.visible = true; + if (!this.visible) { + this.visible = true; + this._onDidChangeVisibility.fire(true); + } } hide(): void { - this.visible = false; + if (this.visible) { + this.visible = false; + this._onDidChangeVisibility.fire(false); + } } isVisible(): boolean { @@ -303,6 +313,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { this.element.tabIndex = 0; this.element.setAttribute('role', 'button'); this.element.setAttribute('aria-haspopup', 'true'); + this.element.setAttribute('aria-expanded', 'false'); this.element.title = this._action.label || ''; return null; @@ -321,6 +332,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { } this.dropdownMenu = this._register(new DropdownMenu(container, options)); + this._register(this.dropdownMenu.onDidChangeVisibility(visible => this.element?.setAttribute('aria-expanded', `${visible}`))); this.dropdownMenu.menuOptions = { actionViewItemProvider: this.actionViewItemProvider, diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index d0e5097f9e6..508fb3dcabd 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -165,7 +165,7 @@ export class PagedList implements IDisposable { } get onDidChangeFocus(): Event> { - return Event.map(this.list.onDidChangeFocus, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); + return Event.map(this.list.onDidChangeFocus, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent })); } get onDidOpen(): Event> { @@ -173,11 +173,11 @@ export class PagedList implements IDisposable { } get onDidChangeSelection(): Event> { - return Event.map(this.list.onDidChangeSelection, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); + return Event.map(this.list.onDidChangeSelection, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent })); } get onPin(): Event> { - return Event.map(this.list.onDidPin, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); + return Event.map(this.list.onDidPin, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent })); } get onContextMenu(): Event> { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 8c9b875936e..b271eea461b 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -26,6 +26,7 @@ import { CombinedSpliceable } from 'vs/base/browser/ui/list/splice'; import { clamp } from 'vs/base/common/numbers'; import { matchesPrefix } from 'vs/base/common/filters'; import { IDragAndDropData } from 'vs/base/browser/dnd'; +import { alert } from 'vs/base/browser/ui/aria/aria'; interface ITraitChangeEvent { indexes: number[]; @@ -344,6 +345,7 @@ class TypeLabelController implements IDisposable { private automaticKeyboardNavigation = true; private triggered = false; + private previouslyFocused = -1; private readonly enabledDisposables = new DisposableStore(); private readonly disposables = new DisposableStore(); @@ -393,6 +395,7 @@ class TypeLabelController implements IDisposable { const onInput = Event.reduce(Event.any(onChar, onClear), (r, i) => i === null ? null : ((r || '') + i)); onInput(this.onInput, this, this.enabledDisposables); + onClear(this.onClear, this, this.enabledDisposables); this.enabled = true; this.triggered = false; @@ -408,6 +411,19 @@ class TypeLabelController implements IDisposable { this.triggered = false; } + private onClear(): void { + const focus = this.list.getFocus(); + if (focus.length > 0 && focus[0] === this.previouslyFocused) { + // List: re-anounce element on typing end since typed keys will interupt aria label of focused element + // Do not announce if there was a focus change at the end to prevent duplication https://github.com/microsoft/vscode/issues/95961 + const ariaLabel = this.list.options.accessibilityProvider?.getAriaLabel(this.list.element(focus[0])); + if (ariaLabel) { + alert(ariaLabel); + } + } + this.previouslyFocused = -1; + } + private onInput(word: string | null): void { if (!word) { this.state = TypeLabelControllerState.Idle; @@ -426,6 +442,7 @@ class TypeLabelController implements IDisposable { const labelStr = label && label.toString(); if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) { + this.previouslyFocused = start; this.list.setFocus([index]); this.list.reveal(index); return; diff --git a/src/vs/base/browser/ui/splitview/paneview.css b/src/vs/base/browser/ui/splitview/paneview.css index 679bf211e94..2694eb4b9d2 100644 --- a/src/vs/base/browser/ui/splitview/paneview.css +++ b/src/vs/base/browser/ui/splitview/paneview.css @@ -16,6 +16,10 @@ flex-direction: column; } +.monaco-pane-view .pane.horizontal:not(.expanded) { + flex-direction: row; +} + .monaco-pane-view .pane > .pane-header { font-size: 11px; font-weight: bold; @@ -26,6 +30,11 @@ align-items: center; } +.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header { + flex-direction: column; + width: 22px; +} + .monaco-pane-view .pane > .pane-header > .twisties { width: 20px; display: flex; @@ -36,6 +45,11 @@ flex-shrink: 0; } +.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header > .twisties { + margin-top: 2px; + margin-bottom: 2px; +} + .monaco-pane-view .pane > .pane-header.expanded > .twisties::before { transform: rotate(90deg); } @@ -132,6 +146,7 @@ width: 100%; height: 100%; min-height: 22px; + min-width: 19px; pointer-events: none; /* very important to not take events away from the parent */ transition: opacity 150ms ease-out; diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index e4c62b298b9..1a7c66cc8a9 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -106,7 +106,7 @@ export abstract class Pane extends Disposable implements IView { get minimumSize(): number { const headerSize = this.headerSize; const expanded = !this.headerVisible || this.isExpanded(); - const minimumBodySize = expanded ? this.minimumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0; + const minimumBodySize = expanded ? this.minimumBodySize : 0; return headerSize + minimumBodySize; } @@ -114,7 +114,7 @@ export abstract class Pane extends Disposable implements IView { get maximumSize(): number { const headerSize = this.headerSize; const expanded = !this.headerVisible || this.isExpanded(); - const maximumBodySize = expanded ? this.maximumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0; + const maximumBodySize = expanded ? this.maximumBodySize : 0; return headerSize + maximumBodySize; } @@ -141,6 +141,10 @@ export abstract class Pane extends Disposable implements IView { return false; } + if (this.element) { + toggleClass(this.element, 'expanded', expanded); + } + this._expanded = !!expanded; this.updateHeader(); @@ -185,12 +189,21 @@ export abstract class Pane extends Disposable implements IView { this._orientation = orientation; + if (this.element) { + toggleClass(this.element, 'horizontal', this.orientation === Orientation.HORIZONTAL); + toggleClass(this.element, 'vertical', this.orientation === Orientation.VERTICAL); + } + if (this.header) { this.updateHeader(); } } render(): void { + toggleClass(this.element, 'expanded', this.isExpanded()); + toggleClass(this.element, 'horizontal', this.orientation === Orientation.HORIZONTAL); + toggleClass(this.element, 'vertical', this.orientation === Orientation.VERTICAL); + this.header = $('.pane-header'); append(this.element, this.header); this.header.setAttribute('tabindex', '0'); @@ -262,7 +275,6 @@ export abstract class Pane extends Disposable implements IView { protected updateHeader(): void { const expanded = !this.headerVisible || this.isExpanded(); - this.header.style.height = `${this.headerSize}px`; this.header.style.lineHeight = `${this.headerSize}px`; toggleClass(this.header, 'hidden', !this.headerVisible); toggleClass(this.header, 'expanded', expanded); diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 8224c7be20e..028e4f839f6 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -951,7 +951,7 @@ export class SplitView extends Disposable { position += this.viewItems[i].size; if (this.sashItems[i].sash === sash) { - return Math.min(position, this.contentSize - 2); + return position; } } diff --git a/src/vs/base/browser/ui/tree/treeDefaults.ts b/src/vs/base/browser/ui/tree/treeDefaults.ts index 944efecd207..834ab449fdd 100644 --- a/src/vs/base/browser/ui/tree/treeDefaults.ts +++ b/src/vs/base/browser/ui/tree/treeDefaults.ts @@ -13,7 +13,7 @@ export class CollapseAllAction extends Action { super('vs.tree.collapse', nls.localize('collapse all', "Collapse All"), 'collapse-all', enabled); } - async run(context?: any): Promise { + async run(): Promise { this.viewer.collapseAll(); this.viewer.setSelection([]); this.viewer.setFocus([]); diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 9951ca26b7e..6925d74b686 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -764,7 +764,7 @@ export class IdleValue { this._handle.dispose(); } - getValue(): T { + get value(): T { if (!this._didRun) { this._handle.dispose(); this._executor(); diff --git a/src/vs/base/common/comparers.ts b/src/vs/base/common/comparers.ts index a713092dbda..a25e2bb9c16 100644 --- a/src/vs/base/common/comparers.ts +++ b/src/vs/base/common/comparers.ts @@ -6,7 +6,12 @@ import { sep } from 'vs/base/common/path'; import { IdleValue } from 'vs/base/common/async'; -const intlFileNameCollator: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => { +// When comparing large numbers of strings, such as in sorting large arrays, is better for +// performance to create an Intl.Collator object and use the function provided by its compare +// property than it is to use String.prototype.localeCompare() + +// A collator with numeric sorting enabled, and no sensitivity to case or to accents +const intlFileNameCollatorBaseNumeric: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); return { collator: collator, @@ -14,20 +19,64 @@ const intlFileNameCollator: IdleValue<{ collator: Intl.Collator, collatorIsNumer }; }); +// A collator with numeric sorting enabled. +const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { + const collator = new Intl.Collator(undefined, { numeric: true }); + return { + collator: collator + }; +}); + +// A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case. +const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { + const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'accent' }); + return { + collator: collator + }; +}); + export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number { const a = one || ''; const b = other || ''; - const result = intlFileNameCollator.getValue().collator.compare(a, b); + const result = intlFileNameCollatorBaseNumeric.value.collator.compare(a, b); // Using the numeric option in the collator will // make compare(`foo1`, `foo01`) === 0. We must disambiguate. - if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && a !== b) { + if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && a !== b) { return a < b ? -1 : 1; } return result; } +/** Compares filenames by name then extension, sorting numbers numerically instead of alphabetically. */ +export function compareFileNamesNumeric(one: string | null, other: string | null): number { + const [oneName, oneExtension] = extractNameAndExtension(one, true); + const [otherName, otherExtension] = extractNameAndExtension(other, true); + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; + let result; + + // Check for name differences, comparing numbers numerically instead of alphabetically. + result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName); + if (result !== 0) { + return result; + } + + // Check for case insensitive extension differences, comparing numbers numerically instead of alphabetically. + result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension); + if (result !== 0) { + return result; + } + + // Disambiguate the extension case if needed. + if (oneExtension !== otherExtension) { + return collatorNumeric.compare(oneExtension, otherExtension); + } + + return 0; +} + const FileNameMatch = /^(.*?)(\.([^.]*))?$/; export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number { @@ -54,19 +103,19 @@ export function compareFileExtensions(one: string | null, other: string | null): const [oneName, oneExtension] = extractNameAndExtension(one); const [otherName, otherExtension] = extractNameAndExtension(other); - let result = intlFileNameCollator.getValue().collator.compare(oneExtension, otherExtension); + let result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneExtension, otherExtension); if (result === 0) { // Using the numeric option in the collator will // make compare(`foo1`, `foo01`) === 0. We must disambiguate. - if (intlFileNameCollator.getValue().collatorIsNumeric && oneExtension !== otherExtension) { + if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && oneExtension !== otherExtension) { return oneExtension < otherExtension ? -1 : 1; } // Extensions are equal, compare filenames - result = intlFileNameCollator.getValue().collator.compare(oneName, otherName); + result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneName, otherName); - if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && oneName !== otherName) { + if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && oneName !== otherName) { return oneName < otherName ? -1 : 1; } } @@ -74,10 +123,63 @@ export function compareFileExtensions(one: string | null, other: string | null): return result; } -function extractNameAndExtension(str?: string | null): [string, string] { +/** Compares filenames by extenson, then by name. Sorts numbers numerically, not alphabetically. */ +export function compareFileExtensionsNumeric(one: string | null, other: string | null): number { + const [oneName, oneExtension] = extractNameAndExtension(one, true); + const [otherName, otherExtension] = extractNameAndExtension(other, true); + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; + let result; + + // Check for extension differences, ignoring differences in case and comparing numbers numerically. + result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension); + if (result !== 0) { + return result; + } + + // Compare names. + result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName); + if (result !== 0) { + return result; + } + + // Disambiguate extension case if needed. + if (oneExtension !== otherExtension) { + return collatorNumeric.compare(oneExtension, otherExtension); + } + + return 0; +} + +/** Extracts the name and extension from a full filename, with optional special handling for dotfiles */ +function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): [string, string] { const match = str ? FileNameMatch.exec(str) as Array : ([] as Array); - return [(match && match[1]) || '', (match && match[3]) || '']; + let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || '']; + + // if the dotfilesAsNames option is selected, treat an empty filename with an extension, + // or a filename that starts with a dot, as a dotfile name + if (dotfilesAsNames && (!result[0] && result[1] || result[0] && result[0].charAt(0) === '.')) { + result = [result[0] + '.' + result[1], '']; + } + + return result; +} + +function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) { + // Check for differences + let result = collator.compare(one, other); + if (result !== 0) { + return result; + } + + // In a numeric comparison, `foo1` and `foo01` will compare as equivalent. + // Disambiguate by sorting the shorter string first. + if (one.length !== other.length) { + return one.length < other.length ? -1 : 1; + } + + return 0; } function comparePathComponents(one: string, other: string, caseSensitive = false): number { diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 5d79ad368a7..8df6ef68bea 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -580,18 +580,23 @@ export const enum Touch { AsNew = 2 } -export class LinkedMap { +export class LinkedMap implements Map { + + readonly [Symbol.toStringTag] = 'LinkedMap'; private _map: Map>; private _head: Item | undefined; private _tail: Item | undefined; private _size: number; + private _state: number; + constructor() { this._map = new Map>(); this._head = undefined; this._tail = undefined; this._size = 0; + this._state = 0; } clear(): void { @@ -599,6 +604,7 @@ export class LinkedMap { this._head = undefined; this._tail = undefined; this._size = 0; + this._state++; } isEmpty(): boolean { @@ -632,7 +638,7 @@ export class LinkedMap { return item.value; } - set(key: K, value: V, touch: Touch = Touch.None): void { + set(key: K, value: V, touch: Touch = Touch.None): this { let item = this._map.get(key); if (item) { item.value = value; @@ -658,6 +664,7 @@ export class LinkedMap { this._map.set(key, item); this._size++; } + return this; } delete(key: K): boolean { @@ -690,6 +697,7 @@ export class LinkedMap { } forEach(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: any): void { + const state = this._state; let current = this._head; while (current) { if (thisArg) { @@ -697,38 +705,25 @@ export class LinkedMap { } else { callbackfn(current.value, current.key, this); } + if (this._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } current = current.next; } } - values(): V[] { - const result: V[] = []; - let current = this._head; - while (current) { - result.push(current.value); - current = current.next; - } - return result; - } - - keys(): K[] { - const result: K[] = []; - let current = this._head; - while (current) { - result.push(current.key); - current = current.next; - } - return result; - } - - /* VS Code / Monaco editor runs on es5 which has no Symbol.iterator keys(): IterableIterator { - const current = this._head; + const map = this; + const state = this._state; + let current = this._head; const iterator: IterableIterator = { [Symbol.iterator]() { return iterator; }, - next():IteratorResult { + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } if (current) { const result = { value: current.key, done: false }; current = current.next; @@ -742,12 +737,17 @@ export class LinkedMap { } values(): IterableIterator { - const current = this._head; + const map = this; + const state = this._state; + let current = this._head; const iterator: IterableIterator = { [Symbol.iterator]() { return iterator; }, - next():IteratorResult { + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } if (current) { const result = { value: current.value, done: false }; current = current.next; @@ -759,7 +759,34 @@ export class LinkedMap { }; return iterator; } - */ + + entries(): IterableIterator<[K, V]> { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator<[K, V]> = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult<[K, V]> { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result: IteratorResult<[K, V]> = { value: [current.key, current.value], done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.entries(); + } protected trimOld(newSize: number) { if (newSize >= this.size) { @@ -781,6 +808,7 @@ export class LinkedMap { if (current) { current.previous = undefined; } + this._state++; } private addItemFirst(item: Item): void { @@ -794,6 +822,7 @@ export class LinkedMap { this._head.previous = item; } this._head = item; + this._state++; } private addItemLast(item: Item): void { @@ -807,6 +836,7 @@ export class LinkedMap { this._tail.next = item; } this._tail = item; + this._state++; } private removeItem(item: Item): void { @@ -843,6 +873,7 @@ export class LinkedMap { } item.next = undefined; item.previous = undefined; + this._state++; } private touch(item: Item, touch: Touch): void { @@ -879,6 +910,7 @@ export class LinkedMap { item.next = this._head; this._head.previous = item; this._head = item; + this._state++; } else if (touch === Touch.AsNew) { if (item === this._tail) { return; @@ -902,6 +934,7 @@ export class LinkedMap { item.previous = this._tail; this._tail.next = item; this._tail = item; + this._state++; } } @@ -953,17 +986,18 @@ export class LRUCache extends LinkedMap { this.checkTrim(); } - get(key: K): V | undefined { - return super.get(key, Touch.AsNew); + get(key: K, touch: Touch = Touch.AsNew): V | undefined { + return super.get(key, touch); } peek(key: K): V | undefined { return super.get(key, Touch.None); } - set(key: K, value: V): void { + set(key: K, value: V): this { super.set(key, value, Touch.AsNew); this.checkTrim(); + return this; } private checkTrim() { diff --git a/src/vs/base/parts/quickinput/browser/media/quickInput.css b/src/vs/base/parts/quickinput/browser/media/quickInput.css index d45840ee38c..c086b587c14 100644 --- a/src/vs/base/parts/quickinput/browser/media/quickInput.css +++ b/src/vs/base/parts/quickinput/browser/media/quickInput.css @@ -103,6 +103,8 @@ .quick-input-count .monaco-count-badge { vertical-align: middle; + padding: 2px 4px; + border-radius: 2px; } .quick-input-action { diff --git a/src/vs/base/test/browser/comparers.test.ts b/src/vs/base/test/browser/comparers.test.ts index 369f160803b..90dff8c2835 100644 --- a/src/vs/base/test/browser/comparers.test.ts +++ b/src/vs/base/test/browser/comparers.test.ts @@ -3,48 +3,294 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareFileNames, compareFileExtensions } from 'vs/base/common/comparers'; +import { compareFileNames, compareFileExtensions, compareFileNamesNumeric, compareFileExtensionsNumeric } from 'vs/base/common/comparers'; import * as assert from 'assert'; +const compareLocale = (a: string, b: string) => a.localeCompare(b); +const compareLocaleNumeric = (a: string, b: string) => a.localeCompare(b, undefined, { numeric: true }); + + suite('Comparers', () => { test('compareFileNames', () => { + // + // Comparisons with the same results as compareFileNamesNumeric + // + + // name-only comparisons assert(compareFileNames(null, null) === 0, 'null should be equal'); assert(compareFileNames(null, 'abc') < 0, 'null should be come before real values'); assert(compareFileNames('', '') === 0, 'empty should be equal'); assert(compareFileNames('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileNames('.abc', '.abc') === 0, 'equal full names should be equal'); - assert(compareFileNames('.env', '.env.example') < 0, 'filenames with extensions should come after those without'); - assert(compareFileNames('.env.example', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNames('z', 'A') > 0, 'z comes is after A regardless of case'); + assert(compareFileNames('Z', 'a') > 0, 'Z comes after a regardless of case'); + + // name plus extension comparisons + assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename'); + + // dotfile comparisons + assert(compareFileNames('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNames('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNames('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNames('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNames('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // dotfile vs non-dotfile comparisons + assert(compareFileNames(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNames('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNames('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNames('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNames('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons assert(compareFileNames('1', '1') === 0, 'numerically equal full names should be equal'); assert(compareFileNames('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); assert(compareFileNames('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); assert(compareFileNames('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileNames('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNames('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + + // + // Comparisons with different results than compareFileNamesNumeric + // + + // name-only comparisons + assert(compareFileNames('a', 'A') !== compareLocale('a', 'A'), 'the same letter does not sort by locale'); + assert(compareFileNames('â', 'Â') !== compareLocale('â', 'Â'), 'the same accented letter does not sort by locale'); + assert.notDeepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNames), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order'); + assert.notDeepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNames), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents do not sort in locale order'); + + // name plus extension comparisons + assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale'); + + // numeric comparisons + assert(compareFileNames('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); + assert(compareFileNames('abc.txt1', 'abc.txt01') > 0, 'same name plus extensions with equal numbers sort in unicode order'); + assert(compareFileNames('art01', 'Art01') !== 'art01'.localeCompare('Art01', undefined, { numeric: true }), + 'a numerically equivalent word of a different case does not compare numerically based on locale'); + }); test('compareFileExtensions', () => { + // + // Comparisons with the same results as compareFileExtensionsNumeric + // + + // name-only comparisons assert(compareFileExtensions(null, null) === 0, 'null should be equal'); - assert(compareFileExtensions(null, '.abc') < 0, 'null should come before real files'); assert(compareFileExtensions(null, 'abc') < 0, 'null should come before real files without extension'); assert(compareFileExtensions('', '') === 0, 'empty should be equal'); assert(compareFileExtensions('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileExtensions('.abc', '.abc') === 0, 'equal full names should be equal'); + assert(compareFileExtensions('z', 'A') > 0, 'z comes after A'); + assert(compareFileExtensions('Z', 'a') > 0, 'Z comes after a'); + + // name plus extension comparisons assert(compareFileExtensions('file.ext', 'file.ext') === 0, 'equal full names should be equal'); assert(compareFileExtensions('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); - assert(compareFileExtensions('.ext', 'a.ext') < 0, 'if equal extensions, filenames should be compared, empty filename should come before others'); - assert(compareFileExtensions('file.aaa', 'file.bbb') < 0, 'files should be compared by extensions'); + assert(compareFileExtensions('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); assert(compareFileExtensions('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extensions even if filenames compare differently'); + assert(compareFileExtensions('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); + assert(compareFileExtensions('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); + assert(compareFileExtensions('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); + + // dotfile comparisons + assert(compareFileExtensions('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensions('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensions(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensions('.env', 'aaa.env') < 0, 'if equal extensions, filenames should be compared, empty filename should come before others'); + assert(compareFileExtensions('.MD', 'a.md') < 0, 'if extensions differ in case, files sort by extension in unicode order'); + + // numeric comparisons assert(compareFileExtensions('1', '1') === 0, 'numerically equal full names should be equal'); assert(compareFileExtensions('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); assert(compareFileExtensions('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); assert(compareFileExtensions('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileExtensions('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensions('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileExtensions('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); assert(compareFileExtensions('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); assert(compareFileExtensions('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); assert(compareFileExtensions('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); - assert(compareFileExtensions('file2.ext2', 'file1.ext10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); - assert(compareFileExtensions('file.ext01', 'file.ext1') < 0, 'extensions with equal numbers should be in alphabetical order'); + assert(compareFileExtensions('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); + + // Same extension comparison that has the same result as compareFileExtensionsNumeric, but a different result than compareFileNames + // This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension. + assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); + + // + // Comparisons with different results from compareFileExtensionsNumeric + // + + // name-only comparisions + assert(compareFileExtensions('a', 'A') !== compareLocale('a', 'A'), 'the same letter of different case does not sort by locale'); + assert(compareFileExtensions('â', 'Â') !== compareLocale('â', 'Â'), 'the same accented letter of different case does not sort by locale'); + assert.notDeepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensions), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order'); + assert.notDeepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensions), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents do not sort in locale order'); + + // name plus extension comparisons + assert(compareFileExtensions('a.MD', 'a.md') !== compareLocale('MD', 'md'), 'case differences in extensions do not sort by locale'); + assert(compareFileExtensions('a.md', 'A.md') !== compareLocale('a', 'A'), 'case differences in names do not sort by locale'); + + // dotfile comparisons + assert(compareFileExtensions('.env', '.aaa.env') < 0, 'a dotfile with an extension is treated as a name plus an extension - equal extensions'); + assert(compareFileExtensions('.env', '.env.aaa') > 0, 'a dotfile with an extension is treated as a name plus an extension - unequal extensions'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensions('.env', 'aaa') > 0, 'filenames without extensions come before dotfiles'); + assert(compareFileExtensions('.md', 'A.MD') > 0, 'a file with an uppercase extension sorts before a dotfile of the same lowercase extension'); + + // numeric comparisons + assert(compareFileExtensions('abc.txt01', 'abc.txt1') < 0, 'extensions with equal numbers sort in unicode order'); + assert(compareFileExtensions('art01', 'Art01') !== compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case does not compare by locale'); + assert(compareFileExtensions('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); + assert(compareFileExtensions('txt.abc01', 'txt.abc1') < 0, 'extensions with equivalent numbers sort in unicode order'); + + }); + + test('compareFileNamesNumeric', () => { + + // + // Comparisons with the same results as compareFileNames + // + + // name-only comparisons + assert(compareFileNamesNumeric(null, null) === 0, 'null should be equal'); + assert(compareFileNamesNumeric(null, 'abc') < 0, 'null should be come before real values'); + assert(compareFileNamesNumeric('', '') === 0, 'empty should be equal'); + assert(compareFileNamesNumeric('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesNumeric('z', 'A') > 0, 'z comes is after A regardless of case'); + assert(compareFileNamesNumeric('Z', 'a') > 0, 'Z comes after a regardless of case'); + + // name plus extension comparisons + assert(compareFileNamesNumeric('file.ext', 'file.ext') === 0, 'equal full names should be equal'); + assert(compareFileNamesNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileNamesNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileNamesNumeric('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + + // dotfile comparisons + assert(compareFileNamesNumeric('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNamesNumeric('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNamesNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesNumeric('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // dotfile vs non-dotfile comparisons + assert(compareFileNamesNumeric(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNamesNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons + assert(compareFileNamesNumeric('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileNamesNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileNamesNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileNamesNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileNamesNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNamesNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + + // + // Comparisons with different results than compareFileNames + // + + // name-only comparisons + assert(compareFileNamesNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter sorts by locale'); + assert(compareFileNamesNumeric('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter sorts by locale'); + assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); + assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesNumeric), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order'); + + // name plus extensions comparisons + assert(compareFileNamesNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'compares the name first, then the extension'); + + // numeric comparisons + assert(compareFileNamesNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); + assert(compareFileNamesNumeric('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); + assert(compareFileNamesNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); + + }); + + test('compareFileExtensionsNumeric', () => { + + // + // Comparisons with the same result as compareFileExtensions + // + + // name-only comparisons + assert(compareFileExtensionsNumeric(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsNumeric(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsNumeric('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsNumeric('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsNumeric('z', 'A') > 0, 'z comes after A'); + assert(compareFileExtensionsNumeric('Z', 'a') > 0, 'Z comes after a'); + + // name plus extension comparisons + assert(compareFileExtensionsNumeric('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileExtensionsNumeric('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsNumeric('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); + assert(compareFileExtensionsNumeric('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); + assert(compareFileExtensionsNumeric('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); + + // dotfile comparisons + assert(compareFileExtensionsNumeric('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsNumeric('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensionsNumeric(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons + assert(compareFileExtensionsNumeric('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileExtensionsNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileExtensionsNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); + assert(compareFileExtensionsNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensionsNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileExtensionsNumeric('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); + assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsNumeric('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileExtensionsNumeric('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); + assert(compareFileExtensionsNumeric('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); + + // Same extension comparison that has the same result as compareFileExtensions, but a different result than compareFileNames + // This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension. + assert(compareFileExtensionsNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); + + // + // Comparisons with different results than compareFileExtensions + // + + // name-only comparisons + assert(compareFileExtensionsNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter of different case sorts by locale'); + assert(compareFileExtensionsNumeric('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter of different case sorts by locale'); + assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); + assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsNumeric), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents sort in locale order'); + + // name plus extension comparisons + assert(compareFileExtensionsNumeric('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); + assert(compareFileExtensionsNumeric('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale'); + + // dotfile comparisons + assert(compareFileExtensionsNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensionsNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + + // numeric comparisons + assert(compareFileExtensionsNumeric('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); + assert(compareFileExtensionsNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); + assert(compareFileExtensionsNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); + assert(compareFileExtensionsNumeric('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); + }); }); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index c9a99858854..7d7f768e3bc 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -13,8 +13,8 @@ suite('Map', () => { let map = new LinkedMap(); map.set('ak', 'av'); map.set('bk', 'bv'); - assert.deepStrictEqual(map.keys(), ['ak', 'bk']); - assert.deepStrictEqual(map.values(), ['av', 'bv']); + assert.deepStrictEqual([...map.keys()], ['ak', 'bk']); + assert.deepStrictEqual([...map.values()], ['av', 'bv']); assert.equal(map.first, 'av'); assert.equal(map.last, 'bv'); }); @@ -23,16 +23,16 @@ suite('Map', () => { let map = new LinkedMap(); map.set('ak', 'av'); map.set('ak', 'av', Touch.AsOld); - assert.deepStrictEqual(map.keys(), ['ak']); - assert.deepStrictEqual(map.values(), ['av']); + assert.deepStrictEqual([...map.keys()], ['ak']); + assert.deepStrictEqual([...map.values()], ['av']); }); test('LinkedMap - Touch New one', () => { let map = new LinkedMap(); map.set('ak', 'av'); map.set('ak', 'av', Touch.AsNew); - assert.deepStrictEqual(map.keys(), ['ak']); - assert.deepStrictEqual(map.values(), ['av']); + assert.deepStrictEqual([...map.keys()], ['ak']); + assert.deepStrictEqual([...map.values()], ['av']); }); test('LinkedMap - Touch Old two', () => { @@ -40,8 +40,8 @@ suite('Map', () => { map.set('ak', 'av'); map.set('bk', 'bv'); map.set('bk', 'bv', Touch.AsOld); - assert.deepStrictEqual(map.keys(), ['bk', 'ak']); - assert.deepStrictEqual(map.values(), ['bv', 'av']); + assert.deepStrictEqual([...map.keys()], ['bk', 'ak']); + assert.deepStrictEqual([...map.values()], ['bv', 'av']); }); test('LinkedMap - Touch New two', () => { @@ -49,8 +49,8 @@ suite('Map', () => { map.set('ak', 'av'); map.set('bk', 'bv'); map.set('ak', 'av', Touch.AsNew); - assert.deepStrictEqual(map.keys(), ['bk', 'ak']); - assert.deepStrictEqual(map.values(), ['bv', 'av']); + assert.deepStrictEqual([...map.keys()], ['bk', 'ak']); + assert.deepStrictEqual([...map.values()], ['bv', 'av']); }); test('LinkedMap - Touch Old from middle', () => { @@ -59,8 +59,8 @@ suite('Map', () => { map.set('bk', 'bv'); map.set('ck', 'cv'); map.set('bk', 'bv', Touch.AsOld); - assert.deepStrictEqual(map.keys(), ['bk', 'ak', 'ck']); - assert.deepStrictEqual(map.values(), ['bv', 'av', 'cv']); + assert.deepStrictEqual([...map.keys()], ['bk', 'ak', 'ck']); + assert.deepStrictEqual([...map.values()], ['bv', 'av', 'cv']); }); test('LinkedMap - Touch New from middle', () => { @@ -69,8 +69,8 @@ suite('Map', () => { map.set('bk', 'bv'); map.set('ck', 'cv'); map.set('bk', 'bv', Touch.AsNew); - assert.deepStrictEqual(map.keys(), ['ak', 'ck', 'bk']); - assert.deepStrictEqual(map.values(), ['av', 'cv', 'bv']); + assert.deepStrictEqual([...map.keys()], ['ak', 'ck', 'bk']); + assert.deepStrictEqual([...map.values()], ['av', 'cv', 'bv']); }); test('LinkedMap - basics', function () { @@ -129,6 +129,61 @@ suite('Map', () => { assert.ok(!map.has('1')); }); + test('LinkedMap - Iterators', () => { + const map = new LinkedMap(); + map.set(1, 1); + map.set(2, 2); + map.set(3, 3); + + for (const elem of map.keys()) { + assert.ok(elem); + } + + for (const elem of map.values()) { + assert.ok(elem); + } + + for (const elem of map.entries()) { + assert.ok(elem); + } + + { + const keys = map.keys(); + const values = map.values(); + const entries = map.entries(); + map.get(1); + keys.next(); + values.next(); + entries.next(); + } + + { + const keys = map.keys(); + const values = map.values(); + const entries = map.entries(); + map.get(1, Touch.AsNew); + + let exceptions: number = 0; + try { + keys.next(); + } catch (err) { + exceptions++; + } + try { + values.next(); + } catch (err) { + exceptions++; + } + try { + entries.next(); + } catch (err) { + exceptions++; + } + + assert.strictEqual(exceptions, 3); + } + }); + test('LinkedMap - LRU Cache simple', () => { const cache = new LRUCache(5); @@ -136,10 +191,10 @@ suite('Map', () => { assert.strictEqual(cache.size, 5); cache.set(6, 6); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual(cache.keys(), [2, 3, 4, 5, 6]); + assert.deepStrictEqual([...cache.keys()], [2, 3, 4, 5, 6]); cache.set(7, 7); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual(cache.keys(), [3, 4, 5, 6, 7]); + assert.deepStrictEqual([...cache.keys()], [3, 4, 5, 6, 7]); let values: number[] = []; [3, 4, 5, 6, 7].forEach(key => values.push(cache.get(key)!)); assert.deepStrictEqual(values, [3, 4, 5, 6, 7]); @@ -150,11 +205,11 @@ suite('Map', () => { [1, 2, 3, 4, 5].forEach(value => cache.set(value, value)); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual(cache.keys(), [1, 2, 3, 4, 5]); + assert.deepStrictEqual([...cache.keys()], [1, 2, 3, 4, 5]); cache.get(3); - assert.deepStrictEqual(cache.keys(), [1, 2, 4, 5, 3]); + assert.deepStrictEqual([...cache.keys()], [1, 2, 4, 5, 3]); cache.peek(4); - assert.deepStrictEqual(cache.keys(), [1, 2, 4, 5, 3]); + assert.deepStrictEqual([...cache.keys()], [1, 2, 4, 5, 3]); let values: number[] = []; [1, 2, 3, 4, 5].forEach(key => values.push(cache.get(key)!)); assert.deepStrictEqual(values, [1, 2, 3, 4, 5]); @@ -169,7 +224,7 @@ suite('Map', () => { assert.strictEqual(cache.size, 10); cache.limit = 5; assert.strictEqual(cache.size, 5); - assert.deepStrictEqual(cache.keys(), [6, 7, 8, 9, 10]); + assert.deepStrictEqual([...cache.keys()], [6, 7, 8, 9, 10]); cache.limit = 20; assert.strictEqual(cache.size, 5); for (let i = 11; i <= 20; i++) { @@ -181,7 +236,7 @@ suite('Map', () => { values.push(cache.get(i)!); assert.strictEqual(cache.get(i), i); } - assert.deepStrictEqual(cache.values(), values); + assert.deepStrictEqual([...cache.values()], values); }); test('LinkedMap - LRU Cache limit with ratio', () => { @@ -193,11 +248,11 @@ suite('Map', () => { assert.strictEqual(cache.size, 10); cache.set(11, 11); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual(cache.keys(), [7, 8, 9, 10, 11]); + assert.deepStrictEqual([...cache.keys()], [7, 8, 9, 10, 11]); let values: number[] = []; - cache.keys().forEach(key => values.push(cache.get(key)!)); + [...cache.keys()].forEach(key => values.push(cache.get(key)!)); assert.deepStrictEqual(values, [7, 8, 9, 10, 11]); - assert.deepStrictEqual(cache.values(), values); + assert.deepStrictEqual([...cache.values()], values); }); test('LinkedMap - toJSON / fromJSON', () => { @@ -222,7 +277,6 @@ suite('Map', () => { assert.equal(key, 'ck'); assert.equal(value, 'cv'); } - i++; }); }); @@ -237,7 +291,7 @@ suite('Map', () => { map.delete('1'); assert.equal(map.get('1'), undefined); assert.equal(map.size, 0); - assert.equal(map.keys().length, 0); + assert.equal([...map.keys()].length, 0); }); test('LinkedMap - delete Head', function () { @@ -251,8 +305,8 @@ suite('Map', () => { map.delete('1'); assert.equal(map.get('2'), 2); assert.equal(map.size, 1); - assert.equal(map.keys().length, 1); - assert.equal(map.keys()[0], 2); + assert.equal([...map.keys()].length, 1); + assert.equal([...map.keys()][0], 2); }); test('LinkedMap - delete Tail', function () { @@ -266,8 +320,8 @@ suite('Map', () => { map.delete('2'); assert.equal(map.get('1'), 1); assert.equal(map.size, 1); - assert.equal(map.keys().length, 1); - assert.equal(map.keys()[0], 1); + assert.equal([...map.keys()].length, 1); + assert.equal([...map.keys()][0], 1); }); diff --git a/src/vs/code/electron-browser/issue/media/issueReporter.css b/src/vs/code/electron-browser/issue/media/issueReporter.css index eb4f53747a8..ad29591ada0 100644 --- a/src/vs/code/electron-browser/issue/media/issueReporter.css +++ b/src/vs/code/electron-browser/issue/media/issueReporter.css @@ -95,25 +95,25 @@ textarea, input, select { } html { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif; color: #CCCCCC; height: 100%; } html:lang(zh-Hans) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } html:lang(zh-Hant) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; } html:lang(ja) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Yu Gothic UI", "Meiryo UI", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Yu Gothic UI", "Meiryo UI", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; } html:lang(ko) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } body { diff --git a/src/vs/code/electron-browser/processExplorer/media/processExplorer.css b/src/vs/code/electron-browser/processExplorer/media/processExplorer.css index 69b0b9a35c9..606ec4c84a8 100644 --- a/src/vs/code/electron-browser/processExplorer/media/processExplorer.css +++ b/src/vs/code/electron-browser/processExplorer/media/processExplorer.css @@ -4,24 +4,24 @@ *--------------------------------------------------------------------------------------------*/ html { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif; font-size: 13px; } html:lang(zh-Hans) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } html:lang(zh-Hant) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; } html:lang(ja) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Yu Gothic UI", "Meiryo UI", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Yu Gothic UI", "Meiryo UI", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; } html:lang(ko) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } body { diff --git a/src/vs/code/electron-browser/proxy/auth.html b/src/vs/code/electron-browser/proxy/auth.html index 5ef195878ca..f0fc7231e34 100644 --- a/src/vs/code/electron-browser/proxy/auth.html +++ b/src/vs/code/electron-browser/proxy/auth.html @@ -5,7 +5,7 @@ + content="default-src 'none'; img-src 'self' https: data:; media-src 'none'; child-src 'self'; object-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https:;"> diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 3a20695a397..fd287c25045 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -125,7 +125,7 @@ export class CodeApplication extends Disposable { // Mac only event: open new window when we get activated if (!hasVisibleWindows && this.windowsMainService) { - this.windowsMainService.openEmptyWindow(OpenContext.DOCK); + this.windowsMainService.openEmptyWindow({ context: OpenContext.DOCK }); } }); @@ -258,7 +258,7 @@ export class CodeApplication extends Disposable { app.on('new-window-for-tab', () => { if (this.windowsMainService) { - this.windowsMainService.openEmptyWindow(OpenContext.DESKTOP); //macOS native tab "+" button + this.windowsMainService.openEmptyWindow({ context: OpenContext.DESKTOP }); //macOS native tab "+" button } }); diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 19c04a25e65..4cd972ef3c3 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2872,8 +2872,8 @@ class EditorScrollbar extends BaseEditorOption { } else if (a instanceof OutlineElement && b instanceof OutlineElement) { if (this.type === OutlineSortOrder.ByKind) { - return a.symbol.kind - b.symbol.kind || this._collator.getValue().compare(a.symbol.name, b.symbol.name); + return a.symbol.kind - b.symbol.kind || this._collator.value.compare(a.symbol.name, b.symbol.name); } else if (this.type === OutlineSortOrder.ByName) { - return this._collator.getValue().compare(a.symbol.name, b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); + return this._collator.value.compare(a.symbol.name, b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); } else if (this.type === OutlineSortOrder.ByPosition) { - return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || this._collator.getValue().compare(a.symbol.name, b.symbol.name); + return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || this._collator.value.compare(a.symbol.name, b.symbol.name); } } return 0; diff --git a/src/vs/editor/contrib/find/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index 4882113bec5..3805801379b 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -126,10 +126,6 @@ justify-content: center; } -.monaco-editor .find-widget .button:not(.disabled):hover { - background-color: rgba(0, 0, 0, 0.1); -} - .monaco-editor .find-widget .button.left { margin-left: 0; margin-right: 3px; @@ -211,11 +207,6 @@ margin-left: -4px; } -.monaco-editor.hc-black .find-widget .button:not(.disabled):hover, -.monaco-editor.vs-dark .find-widget .button:not(.disabled):hover { - background-color: rgba(255, 255, 255, 0.1); -} - .monaco-editor.hc-black .find-widget .button:before { position: relative; top: 1px; diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 6d444d41e65..2372e889922 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -15,7 +15,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, registerInstantiatedEditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { FoldingModel, setCollapseStateAtLevel, CollapseMemento, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateForMatchingLines, setCollapseStateForType, toggleCollapseState, setCollapseStateUp } from 'vs/editor/contrib/folding/foldingModel'; -import { FoldingDecorationProvider } from './foldingDecorations'; +import { FoldingDecorationProvider, foldingCollapsedIcon, foldingExpandedIcon } from './foldingDecorations'; import { FoldingRegions, FoldingRegion } from './foldingRanges'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -909,8 +909,8 @@ registerThemingParticipant((theme, collector) => { const editorFoldColor = theme.getColor(editorFoldForeground); if (editorFoldColor) { collector.addRule(` - .monaco-editor .cldr.codicon-chevron-right, - .monaco-editor .cldr.codicon-chevron-down { + .monaco-editor .cldr${foldingExpandedIcon.cssSelector}, + .monaco-editor .cldr${foldingCollapsedIcon.cssSelector} { color: ${editorFoldColor} !important; } `); diff --git a/src/vs/editor/contrib/folding/foldingDecorations.ts b/src/vs/editor/contrib/folding/foldingDecorations.ts index 1c7861f8ac3..c34e2c2121c 100644 --- a/src/vs/editor/contrib/folding/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/foldingDecorations.ts @@ -9,8 +9,8 @@ import { IDecorationProvider } from 'vs/editor/contrib/folding/foldingModel'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Codicon, registerIcon } from 'vs/base/common/codicons'; -const foldingExpandedIcon = registerIcon('folding-expanded', Codicon.chevronDown); -const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.chevronRight); +export const foldingExpandedIcon = registerIcon('folding-expanded', Codicon.chevronDown); +export const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.chevronRight); export class FoldingDecorationProvider implements IDecorationProvider { diff --git a/src/vs/editor/contrib/gotoError/gotoError.ts b/src/vs/editor/contrib/gotoError/gotoError.ts index c785054b50d..001ed3f273b 100644 --- a/src/vs/editor/contrib/gotoError/gotoError.ts +++ b/src/vs/editor/contrib/gotoError/gotoError.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { Emitter } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IMarker, IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { IMarker } from 'vs/platform/markers/common/markers'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; @@ -18,194 +17,32 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { MarkerNavigationWidget } from './gotoErrorWidget'; -import { compare } from 'vs/base/common/strings'; -import { binarySearch, find } from 'vs/base/common/arrays'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { isEqual } from 'vs/base/common/resources'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { Codicon, registerIcon } from 'vs/base/common/codicons'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; - -class MarkerModel { - - private readonly _editor: ICodeEditor; - private _markers: IMarker[]; - private _nextIdx: number; - private readonly _toUnbind = new DisposableStore(); - private _ignoreSelectionChange: boolean; - private readonly _onCurrentMarkerChanged: Emitter; - private readonly _onMarkerSetChanged: Emitter; - - constructor(editor: ICodeEditor, markers: IMarker[]) { - this._editor = editor; - this._markers = []; - this._nextIdx = -1; - this._ignoreSelectionChange = false; - this._onCurrentMarkerChanged = new Emitter(); - this._onMarkerSetChanged = new Emitter(); - this.setMarkers(markers); - - // listen on editor - this._toUnbind.add(this._editor.onDidDispose(() => this.dispose())); - this._toUnbind.add(this._editor.onDidChangeCursorPosition(() => { - if (this._ignoreSelectionChange) { - return; - } - if (this.currentMarker && this._editor.getPosition() && Range.containsPosition(this.currentMarker, this._editor.getPosition()!)) { - return; - } - this._nextIdx = -1; - })); - } - - public get onCurrentMarkerChanged() { - return this._onCurrentMarkerChanged.event; - } - - public get onMarkerSetChanged() { - return this._onMarkerSetChanged.event; - } - - public setMarkers(markers: IMarker[]): void { - - let oldMarker = this._nextIdx >= 0 ? this._markers[this._nextIdx] : undefined; - this._markers = markers || []; - this._markers.sort(MarkerNavigationAction.compareMarker); - if (!oldMarker) { - this._nextIdx = -1; - } else { - this._nextIdx = Math.max(-1, binarySearch(this._markers, oldMarker, MarkerNavigationAction.compareMarker)); - } - this._onMarkerSetChanged.fire(this); - } - - public withoutWatchingEditorPosition(callback: () => void): void { - this._ignoreSelectionChange = true; - try { - callback(); - } finally { - this._ignoreSelectionChange = false; - } - } - - private _initIdx(fwd: boolean): void { - let found = false; - const position = this._editor.getPosition(); - for (let i = 0; i < this._markers.length; i++) { - let range = Range.lift(this._markers[i]); - - if (range.isEmpty() && this._editor.getModel()) { - const word = this._editor.getModel()!.getWordAtPosition(range.getStartPosition()); - if (word) { - range = new Range(range.startLineNumber, word.startColumn, range.startLineNumber, word.endColumn); - } - } - - if (position && (range.containsPosition(position) || position.isBeforeOrEqual(range.getStartPosition()))) { - this._nextIdx = i; - found = true; - break; - } - } - if (!found) { - // after the last change - this._nextIdx = fwd ? 0 : this._markers.length - 1; - } - if (this._nextIdx < 0) { - this._nextIdx = this._markers.length - 1; - } - } - - get currentMarker(): IMarker | undefined { - return this.canNavigate() ? this._markers[this._nextIdx] : undefined; - } - - set currentMarker(marker: IMarker | undefined) { - const idx = this._nextIdx; - this._nextIdx = -1; - if (marker) { - this._nextIdx = this.indexOf(marker); - } - if (this._nextIdx !== idx) { - this._onCurrentMarkerChanged.fire(marker); - } - } - - public move(fwd: boolean, inCircles: boolean): boolean { - if (!this.canNavigate()) { - this._onCurrentMarkerChanged.fire(undefined); - return !inCircles; - } - - let oldIdx = this._nextIdx; - let atEdge = false; - - if (this._nextIdx === -1) { - this._initIdx(fwd); - - } else if (fwd) { - if (inCircles || this._nextIdx + 1 < this._markers.length) { - this._nextIdx = (this._nextIdx + 1) % this._markers.length; - } else { - atEdge = true; - } - - } else if (!fwd) { - if (inCircles || this._nextIdx > 0) { - this._nextIdx = (this._nextIdx - 1 + this._markers.length) % this._markers.length; - } else { - atEdge = true; - } - } - - if (oldIdx !== this._nextIdx) { - const marker = this._markers[this._nextIdx]; - this._onCurrentMarkerChanged.fire(marker); - } - - return atEdge; - } - - public canNavigate(): boolean { - return this._markers.length > 0; - } - - public findMarkerAtPosition(pos: Position): IMarker | undefined { - return find(this._markers, marker => Range.containsPosition(marker, pos)); - } - - public get total() { - return this._markers.length; - } - - public indexOf(marker: IMarker): number { - return 1 + this._markers.indexOf(marker); - } - - public dispose(): void { - this._toUnbind.dispose(); - } -} +import { IMarkerNavigationService, MarkerList } from 'vs/editor/contrib/gotoError/markerNavigationService'; export class MarkerController implements IEditorContribution { - public static readonly ID = 'editor.contrib.markerController'; + static readonly ID = 'editor.contrib.markerController'; - public static get(editor: ICodeEditor): MarkerController { + static get(editor: ICodeEditor): MarkerController { return editor.getContribution(MarkerController.ID); } private readonly _editor: ICodeEditor; - private _model: MarkerModel | null = null; - private _widget: MarkerNavigationWidget | null = null; + private readonly _widgetVisible: IContextKey; - private readonly _disposeOnClose = new DisposableStore(); + private readonly _sessionDispoables = new DisposableStore(); + + private _model?: MarkerList; + private _widget?: MarkerNavigationWidget; constructor( editor: ICodeEditor, - @IMarkerService private readonly _markerService: IMarkerService, + @IMarkerNavigationService private readonly _markerNavigationService: IMarkerNavigationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ICodeEditorService private readonly _editorService: ICodeEditorService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -214,195 +51,134 @@ export class MarkerController implements IEditorContribution { this._widgetVisible = CONTEXT_MARKERS_NAVIGATION_VISIBLE.bindTo(this._contextKeyService); } - public dispose(): void { + dispose(): void { this._cleanUp(); - this._disposeOnClose.dispose(); + this._sessionDispoables.dispose(); } private _cleanUp(): void { this._widgetVisible.reset(); - this._disposeOnClose.clear(); - this._widget = null; - this._model = null; + this._sessionDispoables.clear(); + this._widget = undefined; + this._model = undefined; } - public getOrCreateModel(): MarkerModel { + private _getOrCreateModel(uri: URI | undefined): MarkerList { - if (this._model) { + if (this._model && this._model.matches(uri)) { return this._model; } + let reusePosition = false; + if (this._model) { + reusePosition = true; + this._cleanUp(); + } - const markers = this._getMarkers(); - this._model = new MarkerModel(this._editor, markers); - this._markerService.onMarkerChanged(this._onMarkerChanged, this, this._disposeOnClose); + this._model = this._markerNavigationService.getMarkerList(uri); + if (reusePosition) { + this._model.move(true, this._editor.getModel()!, this._editor.getPosition()!); + } this._widget = this._instantiationService.createInstance(MarkerNavigationWidget, this._editor); + this._widget.onDidClose(() => this.close(), this, this._sessionDispoables); this._widgetVisible.set(true); - this._widget.onDidClose(() => this.closeMarkersNavigation(), this, this._disposeOnClose); - this._disposeOnClose.add(this._model); - this._disposeOnClose.add(this._widget); + this._sessionDispoables.add(this._model); + this._sessionDispoables.add(this._widget); - this._disposeOnClose.add(this._widget.onDidSelectRelatedInformation(related => { - this._editorService.openCodeEditor({ - resource: related.resource, - options: { pinned: true, revealIfOpened: true, selection: Range.lift(related).collapseToStart() } - }, this._editor).then(undefined, onUnexpectedError); - this.closeMarkersNavigation(false); - })); - this._disposeOnClose.add(this._editor.onDidChangeModel(() => this._cleanUp())); - - this._disposeOnClose.add(this._model.onCurrentMarkerChanged(marker => { - if (!marker || !this._model) { - this._cleanUp(); - } else { - this._model.withoutWatchingEditorPosition(() => { - if (!this._widget || !this._model) { - return; - } - this._widget.showAtMarker(marker, this._model.indexOf(marker), this._model.total); - }); + // follow cursor + this._sessionDispoables.add(this._editor.onDidChangeCursorPosition(e => { + if (!this._model?.selected || !Range.containsPosition(this._model?.selected.marker, e.position)) { + this._model?.resetIndex(); } })); - this._disposeOnClose.add(this._model.onMarkerSetChanged(() => { + + // update markers + this._sessionDispoables.add(this._model.onDidChange(() => { if (!this._widget || !this._widget.position || !this._model) { return; } - - const marker = this._model.findMarkerAtPosition(this._widget.position); - if (marker) { - this._widget.updateMarker(marker); + const info = this._model.find(this._editor.getModel()!.uri, this._widget!.position!); + if (info) { + this._widget.updateMarker(info.marker); } else { this._widget.showStale(); } })); + // open related + this._sessionDispoables.add(this._widget.onDidSelectRelatedInformation(related => { + this._editorService.openCodeEditor({ + resource: related.resource, + options: { pinned: true, revealIfOpened: true, selection: Range.lift(related).collapseToStart() } + }, this._editor); + this.close(false); + })); + this._sessionDispoables.add(this._editor.onDidChangeModel(() => this._cleanUp())); + return this._model; } - public closeMarkersNavigation(focusEditor: boolean = true): void { + close(focusEditor: boolean = true): void { this._cleanUp(); if (focusEditor) { this._editor.focus(); } } - public show(marker: IMarker): void { - const model = this.getOrCreateModel(); - model.currentMarker = marker; + showAtMarker(marker: IMarker): void { + if (this._editor.hasModel()) { + const model = this._getOrCreateModel(this._editor.getModel().uri); + model.resetIndex(); + model.move(true, this._editor.getModel(), new Position(marker.startLineNumber, marker.startColumn)); + if (model.selected) { + this._widget!.showAtMarker(model.selected.marker, model.selected.index, model.selected.total); + } + } } - private _onMarkerChanged(changedResources: readonly URI[]): void { - const editorModel = this._editor.getModel(); - if (!editorModel) { - return; - } + async nagivate(next: boolean, multiFile: boolean) { + if (this._editor.hasModel()) { + const model = this._getOrCreateModel(multiFile ? undefined : this._editor.getModel().uri); + model.move(next, this._editor.getModel(), this._editor.getPosition()); + if (!model.selected) { + return; + } + if (model.selected.marker.resource.toString() !== this._editor.getModel().uri.toString()) { + // show in different editor + this._cleanUp(); + const otherEditor = await this._editorService.openCodeEditor({ + resource: model.selected.marker.resource, + options: { pinned: false, revealIfOpened: true, selectionRevealType: TextEditorSelectionRevealType.NearTop, selection: model.selected.marker } + }, this._editor); - if (!this._model) { - return; - } + if (otherEditor) { + MarkerController.get(otherEditor).close(); + MarkerController.get(otherEditor).nagivate(next, multiFile); + } - if (!changedResources.some(r => isEqual(editorModel.uri, r))) { - return; + } else { + // show in this editor + this._widget!.showAtMarker(model.selected.marker, model.selected.index, model.selected.total); + } } - this._model.setMarkers(this._getMarkers()); - } - - private _getMarkers(): IMarker[] { - let model = this._editor.getModel(); - if (!model) { - return []; - } - - return this._markerService.read({ - resource: model.uri, - severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info - }); } } class MarkerNavigationAction extends EditorAction { - private readonly _isNext: boolean; - - private readonly _multiFile: boolean; - - constructor(next: boolean, multiFile: boolean, opts: IActionOptions) { + constructor( + private readonly _next: boolean, + private readonly _multiFile: boolean, + opts: IActionOptions + ) { super(opts); - this._isNext = next; - this._multiFile = multiFile; } - public run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { - - const markerService = accessor.get(IMarkerService); - const editorService = accessor.get(ICodeEditorService); - const controller = MarkerController.get(editor); - if (!controller) { - return Promise.resolve(undefined); + async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { + if (editor.hasModel()) { + MarkerController.get(editor).nagivate(this._next, this._multiFile); } - - const model = controller.getOrCreateModel(); - const atEdge = model.move(this._isNext, !this._multiFile); - if (!atEdge || !this._multiFile) { - return Promise.resolve(undefined); - } - - // try with the next/prev file - let markers = markerService.read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }).sort(MarkerNavigationAction.compareMarker); - if (markers.length === 0) { - return Promise.resolve(undefined); - } - - const editorModel = editor.getModel(); - if (!editorModel) { - return Promise.resolve(undefined); - } - - let oldMarker = model.currentMarker || { resource: editorModel!.uri, severity: MarkerSeverity.Error, startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }; - let idx = binarySearch(markers, oldMarker, MarkerNavigationAction.compareMarker); - if (idx < 0) { - // find best match... - idx = ~idx; - idx %= markers.length; - } else if (this._isNext) { - idx = (idx + 1) % markers.length; - } else { - idx = (idx + markers.length - 1) % markers.length; - } - - let newMarker = markers[idx]; - if (isEqual(newMarker.resource, editorModel.uri)) { - // the next `resource` is this resource which - // means we cycle within this file - model.move(this._isNext, true); - return Promise.resolve(undefined); - } - - // close the widget for this editor-instance, open the resource - // for the next marker and re-start marker navigation in there - controller.closeMarkersNavigation(); - - return editorService.openCodeEditor({ - resource: newMarker.resource, - options: { pinned: false, revealIfOpened: true, selectionRevealType: TextEditorSelectionRevealType.NearTop, selection: newMarker } - }, editor).then(editor => { - if (!editor) { - return undefined; - } - return editor.getAction(this.id).run(); - }); - } - - static compareMarker(a: IMarker, b: IMarker): number { - let res = compare(a.resource.toString(), b.resource.toString()); - if (res === 0) { - res = MarkerSeverity.compare(a.severity, b.severity); - } - if (res === 0) { - res = Range.compareRangesUsingStarts(a, b); - } - return res; } } @@ -467,6 +243,12 @@ class NextMarkerInFilesAction extends MarkerNavigationAction { kbExpr: EditorContextKeys.focus, primary: KeyCode.F8, weight: KeybindingWeight.EditorContrib + }, + menuOpts: { + menuId: MenuId.MenubarGoMenu, + title: nls.localize({ key: 'miGotoNextProblem', comment: ['&& denotes a mnemonic'] }, "Next &&Problem"), + group: '6_problem_nav', + order: 1 } }); } @@ -483,6 +265,12 @@ class PrevMarkerInFilesAction extends MarkerNavigationAction { kbExpr: EditorContextKeys.focus, primary: KeyMod.Shift | KeyCode.F8, weight: KeybindingWeight.EditorContrib + }, + menuOpts: { + menuId: MenuId.MenubarGoMenu, + title: nls.localize({ key: 'miGotoPreviousProblem', comment: ['&& denotes a mnemonic'] }, "Previous &&Problem"), + group: '6_problem_nav', + order: 2 } }); } @@ -501,7 +289,7 @@ const MarkerCommand = EditorCommand.bindToContribution(MarkerC registerEditorCommand(new MarkerCommand({ id: 'closeMarkersNavigation', precondition: CONTEXT_MARKERS_NAVIGATION_VISIBLE, - handler: x => x.closeMarkersNavigation(), + handler: x => x.close(), kbOpts: { weight: KeybindingWeight.EditorContrib + 50, kbExpr: EditorContextKeys.focus, @@ -509,22 +297,3 @@ registerEditorCommand(new MarkerCommand({ secondary: [KeyMod.Shift | KeyCode.Escape] } })); - -// Go to menu -MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { - group: '6_problem_nav', - command: { - id: 'editor.action.marker.nextInFiles', - title: nls.localize({ key: 'miGotoNextProblem', comment: ['&& denotes a mnemonic'] }, "Next &&Problem") - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { - group: '6_problem_nav', - command: { - id: 'editor.action.marker.prevInFiles', - title: nls.localize({ key: 'miGotoPreviousProblem', comment: ['&& denotes a mnemonic'] }, "Previous &&Problem") - }, - order: 2 -}); diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 76ffd641d15..7c2eed05550 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -8,7 +8,6 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers'; -import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerColor, oneOf, textLinkForeground, editorErrorForeground, editorErrorBorder, editorWarningForeground, editorWarningBorder, editorInfoForeground, editorInfoBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -296,6 +295,9 @@ export class MarkerNavigationWidget extends PeekViewWidget { protected _fillHead(container: HTMLElement): void { super._fillHead(container); + + this._disposables.add(this._actionbarWidget!.actionRunner.onDidBeforeRun(e => this.editor.focus())); + const actions: IAction[] = []; const menu = this._menuService.createMenu(MarkerNavigationWidget.TitleMenu, this._contextKeyService); createAndFillInActionBarActions(menu, undefined, actions); @@ -327,7 +329,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._disposables.add(this._message); } - show(where: Position, heightInLines: number): void { + show(): void { throw new Error('call showAtMarker'); } diff --git a/src/vs/editor/contrib/gotoError/markerNavigationService.ts b/src/vs/editor/contrib/gotoError/markerNavigationService.ts new file mode 100644 index 00000000000..abf0fe7a4e0 --- /dev/null +++ b/src/vs/editor/contrib/gotoError/markerNavigationService.ts @@ -0,0 +1,217 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IMarkerService, MarkerSeverity, IMarker } from 'vs/platform/markers/common/markers'; +import { URI } from 'vs/base/common/uri'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { compare } from 'vs/base/common/strings'; +import { binarySearch } from 'vs/base/common/arrays'; +import { ITextModel } from 'vs/editor/common/model'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { LinkedList } from 'vs/base/common/linkedList'; + +export class MarkerCoordinate { + constructor( + readonly marker: IMarker, + readonly index: number, + readonly total: number + ) { } +} + +export class MarkerList { + + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + private readonly _resourceFilter?: (uri: URI) => boolean; + private readonly _dispoables = new DisposableStore(); + + private _markers: IMarker[] = []; + private _nextIdx: number = -1; + + constructor( + resourceFilter: URI | ((uri: URI) => boolean) | undefined, + @IMarkerService private readonly _markerService: IMarkerService, + ) { + if (URI.isUri(resourceFilter)) { + this._resourceFilter = uri => uri.toString() === resourceFilter.toString(); + } else if (resourceFilter) { + this._resourceFilter = resourceFilter; + } + + const updateMarker = () => { + this._markers = this._markerService.read({ + resource: URI.isUri(resourceFilter) ? resourceFilter : undefined, + severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info + }); + if (typeof resourceFilter === 'function') { + this._markers = this._markers.filter(m => this._resourceFilter!(m.resource)); + } + this._markers.sort(MarkerList._compareMarker); + }; + + updateMarker(); + + this._dispoables.add(_markerService.onMarkerChanged(uris => { + if (!this._resourceFilter || uris.some(uri => this._resourceFilter!(uri))) { + updateMarker(); + this._nextIdx = -1; + this._onDidChange.fire(); + } + })); + } + + dispose(): void { + this._dispoables.dispose(); + this._onDidChange.dispose(); + } + + matches(uri: URI | undefined) { + if (!this._resourceFilter && !uri) { + return true; + } + if (!this._resourceFilter || !uri) { + return false; + } + return this._resourceFilter(uri); + } + + get selected(): MarkerCoordinate | undefined { + const marker = this._markers[this._nextIdx]; + return marker && new MarkerCoordinate(marker, this._nextIdx + 1, this._markers.length); + } + + private _initIdx(model: ITextModel, position: Position, fwd: boolean): void { + let found = false; + + let idx = this._markers.findIndex(marker => marker.resource.toString() === model.uri.toString()); + if (idx < 0) { + idx = binarySearch(this._markers, { resource: model.uri }, (a, b) => compare(a.resource.toString(), b.resource.toString())); + if (idx < 0) { + idx = ~idx; + } + } + + for (let i = idx; i < this._markers.length; i++) { + let range = Range.lift(this._markers[i]); + + if (range.isEmpty()) { + const word = model.getWordAtPosition(range.getStartPosition()); + if (word) { + range = new Range(range.startLineNumber, word.startColumn, range.startLineNumber, word.endColumn); + } + } + + if (position && (range.containsPosition(position) || position.isBeforeOrEqual(range.getStartPosition()))) { + this._nextIdx = i; + found = true; + break; + } + + if (this._markers[i].resource.toString() !== model.uri.toString()) { + break; + } + } + + if (!found) { + // after the last change + this._nextIdx = fwd ? 0 : this._markers.length - 1; + } + if (this._nextIdx < 0) { + this._nextIdx = this._markers.length - 1; + } + } + + resetIndex() { + this._nextIdx = -1; + } + + move(fwd: boolean, model: ITextModel, position: Position): boolean { + if (this._markers.length === 0) { + return false; + } + + let oldIdx = this._nextIdx; + if (this._nextIdx === -1) { + this._initIdx(model, position, fwd); + } else if (fwd) { + this._nextIdx = (this._nextIdx + 1) % this._markers.length; + } else if (!fwd) { + this._nextIdx = (this._nextIdx - 1 + this._markers.length) % this._markers.length; + } + + if (oldIdx !== this._nextIdx) { + return true; + } + return false; + } + + find(uri: URI, position: Position): MarkerCoordinate | undefined { + let idx = this._markers.findIndex(marker => marker.resource.toString() === uri.toString()); + if (idx < 0) { + return undefined; + } + for (; idx < this._markers.length; idx++) { + if (Range.containsPosition(this._markers[idx], position)) { + return new MarkerCoordinate(this._markers[idx], idx + 1, this._markers.length); + } + } + return undefined; + } + + private static _compareMarker(a: IMarker, b: IMarker): number { + let res = compare(a.resource.toString(), b.resource.toString()); + if (res === 0) { + res = MarkerSeverity.compare(a.severity, b.severity); + } + if (res === 0) { + res = Range.compareRangesUsingStarts(a, b); + } + return res; + } +} + +export const IMarkerNavigationService = createDecorator('IMarkerNavigationService'); + +export interface IMarkerNavigationService { + readonly _serviceBrand: undefined; + registerProvider(provider: IMarkerListProvider): IDisposable; + getMarkerList(resource: URI | undefined): MarkerList; +} + +export interface IMarkerListProvider { + getMarkerList(resource: URI | undefined): MarkerList | undefined; +} + +class MarkerNavigationService implements IMarkerNavigationService, IMarkerListProvider { + + readonly _serviceBrand: undefined; + + private readonly _provider = new LinkedList(); + + constructor(@IMarkerService private readonly _markerService: IMarkerService) { } + + registerProvider(provider: IMarkerListProvider): IDisposable { + const remove = this._provider.unshift(provider); + return toDisposable(() => remove()); + } + + getMarkerList(resource: URI | undefined): MarkerList { + for (let provider of this._provider) { + const result = provider.getMarkerList(resource); + if (result) { + return result; + } + } + // default + return new MarkerList(resource, this._markerService); + } +} + +registerSingleton(IMarkerNavigationService, MarkerNavigationService, true); diff --git a/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css b/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css index c748c365bdc..bf9d507df0a 100644 --- a/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css +++ b/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css @@ -32,7 +32,7 @@ user-select: text; -webkit-user-select: text; -ms-user-select: text; - padding: 8px 12px 0px 20px; + padding: 8px 12px 0 20px; } .monaco-editor .marker-widget .descriptioncontainer .message { diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index 27e5d9b096d..71055810784 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -567,7 +567,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { commandId: NextMarkerAction.ID, run: () => { this.hide(); - MarkerController.get(this._editor).show(markerHover.marker); + MarkerController.get(this._editor).showAtMarker(markerHover.marker); this._editor.focus(); } })); @@ -686,4 +686,3 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-editor-hover .hover-contents a.code-link span:hover { color: ${linkFg}; }`); } }); - diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index d09f3e7a246..809192427fa 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -296,10 +296,10 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { private getParameterLabel(signature: modes.SignatureInformation, paramIdx: number): string { const param = signature.parameters[paramIdx]; - if (typeof param.label === 'string') { - return param.label; - } else { + if (Array.isArray(param.label)) { return signature.label.substring(param.label[0], param.label[1]); + } else { + return param.label; } } diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index ae87a914c9b..865d05eccdf 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -182,7 +182,7 @@ class RenameController implements IEditorContribution { } const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue(this.editor.getModel().uri, 'editor.rename.enablePreview'); - const inputFieldResult = await this._renameInputField.getValue().getInput(loc.range, loc.text, selectionStart, selectionEnd, supportPreview, this._cts.token); + const inputFieldResult = await this._renameInputField.value.getInput(loc.range, loc.text, selectionStart, selectionEnd, supportPreview, this._cts.token); // no result, only hint to focus the editor or not if (typeof inputFieldResult === 'boolean') { @@ -230,11 +230,11 @@ class RenameController implements IEditorContribution { } acceptRenameInput(wantsPreview: boolean): void { - this._renameInputField.getValue().acceptInput(wantsPreview); + this._renameInputField.value.acceptInput(wantsPreview); } cancelRenameInput(): void { - this._renameInputField.getValue().cancelInput(true); + this._renameInputField.value.cancelInput(true); } } diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 913c86fb47a..d19c3ce2b09 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -204,18 +204,18 @@ export class SuggestController implements IEditorContribution { this._toDispose.add(_instantiationService.createInstance(WordContextKey, editor)); this._toDispose.add(this.model.onDidTrigger(e => { - this.widget.getValue().showTriggered(e.auto, e.shy ? 250 : 50); + this.widget.value.showTriggered(e.auto, e.shy ? 250 : 50); this._lineSuffix.value = new LineSuffix(this.editor.getModel()!, e.position); })); this._toDispose.add(this.model.onDidSuggest(e => { if (!e.shy) { let index = this._memoryService.select(this.editor.getModel()!, this.editor.getPosition()!, e.completionModel.items); - this.widget.getValue().showSuggestions(e.completionModel, index, e.isFrozen, e.auto); + this.widget.value.showSuggestions(e.completionModel, index, e.isFrozen, e.auto); } })); this._toDispose.add(this.model.onDidCancel(e => { if (!e.retrigger) { - this.widget.getValue().hideWidget(); + this.widget.value.hideWidget(); } })); this._toDispose.add(this.editor.onDidBlurEditorWidget(() => { @@ -248,7 +248,7 @@ export class SuggestController implements IEditorContribution { flags: InsertFlags ): void { if (!event || !event.item) { - this._alternatives.getValue().reset(); + this._alternatives.value.reset(); this.model.cancel(); this.model.clear(); return; @@ -260,7 +260,6 @@ export class SuggestController implements IEditorContribution { const model = this.editor.getModel(); const modelVersionNow = model.getAlternativeVersionId(); const { item } = event; - const { completion: suggestion } = item; // pushing undo stops *before* additional text edits and // *after* the main edit @@ -276,12 +275,12 @@ export class SuggestController implements IEditorContribution { const scrollState = StableEditorScrollState.capture(this.editor); - if (Array.isArray(suggestion.additionalTextEdits)) { - this.editor.executeEdits('suggestController.additionalTextEdits', suggestion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); + if (Array.isArray(item.completion.additionalTextEdits)) { + this.editor.executeEdits('suggestController.additionalTextEdits', item.completion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); } - let { insertText } = suggestion; - if (!(suggestion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)) { + let { insertText } = item.completion; + if (!(item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)) { insertText = SnippetParser.escape(insertText); } @@ -290,7 +289,7 @@ export class SuggestController implements IEditorContribution { overwriteAfter: info.overwriteAfter, undoStopBefore: false, undoStopAfter: false, - adjustWhitespace: !(suggestion.insertTextRules! & CompletionItemInsertTextRule.KeepWhitespace) + adjustWhitespace: !(item.completion.insertTextRules! & CompletionItemInsertTextRule.KeepWhitespace) }); scrollState.restoreRelativeVerticalPositionOfCursor(this.editor); @@ -299,25 +298,25 @@ export class SuggestController implements IEditorContribution { this.editor.pushUndoStop(); } - if (!suggestion.command) { + if (!item.completion.command) { // done this.model.cancel(); this.model.clear(); - } else if (suggestion.command.id === TriggerSuggestAction.id) { + } else if (item.completion.command.id === TriggerSuggestAction.id) { // retigger this.model.trigger({ auto: true, shy: false }, true); } else { // exec command, done - this._commandService.executeCommand(suggestion.command.id, ...(suggestion.command.arguments ? [...suggestion.command.arguments] : [])) + this._commandService.executeCommand(item.completion.command.id, ...(item.completion.command.arguments ? [...item.completion.command.arguments] : [])) .catch(onUnexpectedError) .finally(() => this.model.clear()); // <- clear only now, keep commands alive this.model.cancel(); } if (flags & InsertFlags.KeepAlternativeSuggestions) { - this._alternatives.getValue().set(event, next => { + this._alternatives.value.set(event, next => { // this is not so pretty. when inserting the 'next' // suggestion we undo until we are at the state at // which we were before inserting the previous suggestion... @@ -334,7 +333,7 @@ export class SuggestController implements IEditorContribution { }); } - this._alertCompletionItem(event.item); + this._alertCompletionItem(item); } getOverwriteInfo(item: CompletionItem, toggleMode: boolean): { overwriteBefore: number, overwriteAfter: number } { @@ -440,7 +439,7 @@ export class SuggestController implements IEditorContribution { } acceptSelectedSuggestion(keepAlternativeSuggestions: boolean, alternativeOverwriteConfig: boolean): void { - const item = this.widget.getValue().getFocusedItem(); + const item = this.widget.value.getFocusedItem(); let flags = 0; if (keepAlternativeSuggestions) { flags |= InsertFlags.KeepAlternativeSuggestions; @@ -451,53 +450,53 @@ export class SuggestController implements IEditorContribution { this._insertSuggestion(item, flags); } acceptNextSuggestion() { - this._alternatives.getValue().next(); + this._alternatives.value.next(); } acceptPrevSuggestion() { - this._alternatives.getValue().prev(); + this._alternatives.value.prev(); } cancelSuggestWidget(): void { this.model.cancel(); this.model.clear(); - this.widget.getValue().hideWidget(); + this.widget.value.hideWidget(); } selectNextSuggestion(): void { - this.widget.getValue().selectNext(); + this.widget.value.selectNext(); } selectNextPageSuggestion(): void { - this.widget.getValue().selectNextPage(); + this.widget.value.selectNextPage(); } selectLastSuggestion(): void { - this.widget.getValue().selectLast(); + this.widget.value.selectLast(); } selectPrevSuggestion(): void { - this.widget.getValue().selectPrevious(); + this.widget.value.selectPrevious(); } selectPrevPageSuggestion(): void { - this.widget.getValue().selectPreviousPage(); + this.widget.value.selectPreviousPage(); } selectFirstSuggestion(): void { - this.widget.getValue().selectFirst(); + this.widget.value.selectFirst(); } toggleSuggestionDetails(): void { - this.widget.getValue().toggleDetails(); + this.widget.value.toggleDetails(); } toggleExplainMode(): void { - this.widget.getValue().toggleExplainMode(); + this.widget.value.toggleExplainMode(); } toggleSuggestionFocus(): void { - this.widget.getValue().toggleDetailsFocus(); + this.widget.value.toggleDetailsFocus(); } } diff --git a/src/vs/editor/standalone/browser/standalone-tokens.css b/src/vs/editor/standalone/browser/standalone-tokens.css index 6cc916c0aa2..4d173457365 100644 --- a/src/vs/editor/standalone/browser/standalone-tokens.css +++ b/src/vs/editor/standalone/browser/standalone-tokens.css @@ -6,7 +6,7 @@ /* Default standalone editor font */ .monaco-editor { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif; } .monaco-menu .monaco-action-bar.vertical .action-item .action-menu-item:focus .action-label { diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index c20e8327058..261a598fcfb 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -477,7 +477,7 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable { disposables.add(MenuRegistry.appendMenuItem(menu.id, { command, ...menu })); } if (f1) { - disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command })); + disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, when: command.precondition })); } // keybinding diff --git a/src/vs/platform/credentials/node/credentialsService.ts b/src/vs/platform/credentials/node/credentialsService.ts index bc8d8d75914..3641cf65d33 100644 --- a/src/vs/platform/credentials/node/credentialsService.ts +++ b/src/vs/platform/credentials/node/credentialsService.ts @@ -14,27 +14,27 @@ export class KeytarCredentialsService implements ICredentialsService { private readonly _keytar = new IdleValue>(() => import('keytar')); async getPassword(service: string, account: string): Promise { - const keytar = await this._keytar.getValue(); + const keytar = await this._keytar.value; return keytar.getPassword(service, account); } async setPassword(service: string, account: string, password: string): Promise { - const keytar = await this._keytar.getValue(); + const keytar = await this._keytar.value; return keytar.setPassword(service, account, password); } async deletePassword(service: string, account: string): Promise { - const keytar = await this._keytar.getValue(); + const keytar = await this._keytar.value; return keytar.deletePassword(service, account); } async findPassword(service: string): Promise { - const keytar = await this._keytar.getValue(); + const keytar = await this._keytar.value; return keytar.findPassword(service); } async findCredentials(service: string): Promise> { - const keytar = await this._keytar.getValue(); + const keytar = await this._keytar.value; return keytar.findCredentials(service); } } diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index a8cc247f89d..c21fb966278 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -114,7 +114,10 @@ export class ElectronMainService implements IElectronMainService { } private async doOpenEmptyWindow(windowId: number | undefined, options?: IOpenEmptyWindowOptions): Promise { - this.windowsMainService.openEmptyWindow(OpenContext.API, options); + this.windowsMainService.openEmptyWindow({ + context: OpenContext.API, + contextWindowId: windowId + }, options); } async toggleFullScreen(windowId: number | undefined): Promise { diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index cf9492900e9..c16ed7b4e9d 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -219,7 +219,7 @@ export class InstantiationService implements IInstantiationService { if (key in target) { return target[key]; } - let obj = idle.getValue(); + let obj = idle.value; let prop = obj[key]; if (typeof prop !== 'function') { return prop; @@ -229,7 +229,7 @@ export class InstantiationService implements IInstantiationService { return prop; }, set(_target: T, p: PropertyKey, value: any): boolean { - idle.getValue()[p] = value; + idle.value[p] = value; return true; } }); diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 17698833e34..8775dd82ad2 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -6,7 +6,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IURLService } from 'vs/platform/url/common/url'; import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ParsedArgs } from 'vs/platform/environment/node/argv'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWindowSettings } from 'vs/platform/windows/common/windows'; @@ -56,7 +55,6 @@ export interface ILaunchMainService { start(args: ParsedArgs, userEnv: IProcessEnvironment): Promise; getMainProcessId(): Promise; getMainProcessInfo(): Promise; - getLogsPath(): Promise; getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]>; } @@ -69,7 +67,6 @@ export class LaunchMainService implements ILaunchMainService { @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IURLService private readonly urlService: IURLService, @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, @IConfigurationService private readonly configurationService: IConfigurationService ) { } @@ -84,7 +81,7 @@ export class LaunchMainService implements ILaunchMainService { // Create a window if there is none if (this.windowsMainService.getWindowCount() === 0) { - const window = this.windowsMainService.openEmptyWindow(OpenContext.DESKTOP)[0]; + const window = this.windowsMainService.openEmptyWindow({ context: OpenContext.DESKTOP })[0]; whenWindowReady = window.ready(); } @@ -226,12 +223,6 @@ export class LaunchMainService implements ILaunchMainService { }); } - getLogsPath(): Promise { - this.logService.trace('Received request for logs path from other instance.'); - - return Promise.resolve(this.environmentService.logsPath); - } - getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { const windows = this.windowsMainService.getWindows(); const promises: Promise[] = windows.map(window => { diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index f346cd655fd..f81f8534749 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -531,7 +531,7 @@ abstract class ResourceNavigator extends Disposable { !!(browserEvent).preserveFocus : !isDoubleClick; - if (this.treeOrList.openOnSingleClick || isDoubleClick || isKeyboardEvent) { + if (this.options.openOnSingleClick || this.treeOrList.openOnSingleClick || isDoubleClick || isKeyboardEvent) { const sideBySide = browserEvent instanceof MouseEvent && (browserEvent.ctrlKey || browserEvent.metaKey || browserEvent.altKey); this.open(preserveFocus, isDoubleClick || isMiddleClick, sideBySide, browserEvent); } diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index dd0251290f2..ac66913edbf 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -61,7 +61,7 @@ export class Menubar { private keybindings: { [commandId: string]: IMenubarKeybinding }; - private fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow, event: Event) => void } = {}; + private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow, event: Event) => void } = Object.create(null); constructor( @IUpdateService private readonly updateService: IUpdateService, @@ -113,8 +113,8 @@ export class Menubar { private addFallbackHandlers(): void { // File Menu Items - this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = () => this.windowsMainService.openEmptyWindow(OpenContext.MENU); - this.fallbackMenuHandlers['workbench.action.newWindow'] = () => this.windowsMainService.openEmptyWindow(OpenContext.MENU); + this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win.id }); + this.fallbackMenuHandlers['workbench.action.newWindow'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win.id }); this.fallbackMenuHandlers['workbench.action.files.openFileFolder'] = (menuItem, win, event) => this.electronMainService.pickFileFolderAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }); this.fallbackMenuHandlers['workbench.action.openWorkspace'] = (menuItem, win, event) => this.electronMainService.pickWorkspaceAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }); @@ -266,7 +266,7 @@ export class Menubar { this.appMenuInstalled = true; const dockMenu = new Menu(); - dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openEmptyWindow(OpenContext.DOCK) })); + dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openEmptyWindow({ context: OpenContext.DOCK }) })); app.dock.setMenu(dockMenu); } diff --git a/src/vs/platform/menubar/electron-main/menubarMainService.ts b/src/vs/platform/menubar/electron-main/menubarMainService.ts index 3cdb14892c0..7cadd57ecdb 100644 --- a/src/vs/platform/menubar/electron-main/menubarMainService.ts +++ b/src/vs/platform/menubar/electron-main/menubarMainService.ts @@ -13,28 +13,26 @@ export class MenubarMainService implements IMenubarService { _serviceBrand: undefined; - private _menubar: Menubar | undefined; + private menubar: Promise; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @ILogService private readonly logService: ILogService ) { - // Install Menu - this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { - this._menubar = this.instantiationService.createInstance(Menubar); - }); + this.menubar = this.installMenuBarAfterWindowOpen(); } - updateMenubar(windowId: number, menus: IMenubarData): Promise { - return this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { - this.logService.trace('menubarService#updateMenubar', windowId); + private async installMenuBarAfterWindowOpen(): Promise { + await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen); - if (this._menubar) { - this._menubar.updateMenu(menus, windowId); - } + return this.instantiationService.createInstance(Menubar); + } - return undefined; - }); + async updateMenubar(windowId: number, menus: IMenubarData): Promise { + this.logService.trace('menubarService#updateMenubar', windowId); + + const menubar = await this.menubar; + menubar.updateMenu(menus, windowId); } } diff --git a/src/vs/platform/theme/common/tokenClassificationRegistry.ts b/src/vs/platform/theme/common/tokenClassificationRegistry.ts index 14a1b52e4c3..754c93d5bd7 100644 --- a/src/vs/platform/theme/common/tokenClassificationRegistry.ts +++ b/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -515,7 +515,7 @@ function createDefaultTokenClassificationRegistry(): TokenClassificationRegistry registerTokenType('namespace', nls.localize('namespace', "Style for namespaces."), [['entity.name.namespace']]); registerTokenType('type', nls.localize('type', "Style for types."), [['entity.name.type'], ['support.type']]); - registerTokenType('struct', nls.localize('struct', "Style for structs."), [['storage.type.struct']]); + registerTokenType('struct', nls.localize('struct', "Style for structs."), [['entity.name.type.struct']]); registerTokenType('class', nls.localize('class', "Style for classes."), [['entity.name.type.class'], ['support.class']]); registerTokenType('interface', nls.localize('interface', "Style for interfaces."), [['entity.name.type.interface']]); registerTokenType('enum', nls.localize('enum', "Style for enums."), [['entity.name.type.enum']]); diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index e52d759f8e4..4860d158671 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; -import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources'; import { CancelablePromise } from 'vs/base/common/async'; @@ -108,7 +108,7 @@ export abstract class AbstractSynchroniser extends Disposable { protected isEnabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resource); } - async sync(ref?: string): Promise { + async sync(manifest: IUserDataManifest | null): Promise { if (!this.isEnabled()) { if (this.status !== SyncStatus.Idle) { await this.stop(); @@ -129,7 +129,7 @@ export abstract class AbstractSynchroniser extends Disposable { this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); - const remoteUserData = ref && lastSyncUserData && lastSyncUserData.ref === ref ? lastSyncUserData : await this.getRemoteUserData(lastSyncUserData); + const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData); let status: SyncStatus = SyncStatus.Idle; try { @@ -144,6 +144,24 @@ export abstract class AbstractSynchroniser extends Disposable { } } + private async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise { + if (lastSyncUserData) { + + const latestRef = manifest && manifest.latest ? manifest.latest[this.resource] : undefined; + + // Last time synced resource and latest resource on server are same + if (lastSyncUserData.ref === latestRef) { + return lastSyncUserData; + } + + // There is no resource on server and last time it was synced with no resource + if (latestRef === undefined && lastSyncUserData.syncData === null) { + return lastSyncUserData; + } + } + return this.getRemoteUserData(lastSyncUserData); + } + async getSyncPreview(): Promise { if (!this.isEnabled()) { return { hasLocalChanged: false, hasRemoteChanged: false }; @@ -225,15 +243,19 @@ export abstract class AbstractSynchroniser extends Disposable { } catch (e) { /* ignore */ } } - protected async getLastSyncUserData(): Promise { + async getLastSyncUserData(): Promise { try { const content = await this.fileService.readFile(this.lastSyncResource); const parsed = JSON.parse(content.value.toString()); - let syncData: ISyncData = JSON.parse(parsed.content); + const userData: IUserData = parsed as IUserData; + if (userData.content === null) { + return { ref: parsed.ref, syncData: null } as T; + } + let syncData: ISyncData = JSON.parse(userData.content); // Migration from old content to sync data if (!isSyncData(syncData)) { - syncData = { version: this.version, content: parsed.content }; + syncData = { version: this.version, content: userData.content }; } return { ...parsed, ...{ syncData, content: undefined } }; @@ -247,11 +269,11 @@ export abstract class AbstractSynchroniser extends Disposable { } protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary = {}): Promise { - const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: JSON.stringify(lastSyncRemoteUserData.syncData), ...additionalProps }; + const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps }; await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); } - protected async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise { + async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise { const { ref, content } = await this.getUserData(lastSyncData); let syncData: ISyncData | null = null; if (content !== null) { diff --git a/src/vs/platform/userDataSync/common/extensionsMerge.ts b/src/vs/platform/userDataSync/common/extensionsMerge.ts index bb02ff61d84..43f246b5733 100644 --- a/src/vs/platform/userDataSync/common/extensionsMerge.ts +++ b/src/vs/platform/userDataSync/common/extensionsMerge.ts @@ -21,11 +21,12 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync const updated: ISyncExtension[] = []; if (!remoteExtensions) { + const remote = localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase())); return { added, removed, updated, - remote: localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase())) + remote: remote.length > 0 ? remote : null }; } diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 33901ed48ce..ae3e0ba4da8 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -16,9 +16,10 @@ import { isNonEmptyArray } from 'vs/base/common/arrays'; import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; -import { joinPath, dirname, basename } from 'vs/base/common/resources'; +import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources'; import { format } from 'vs/base/common/jsonFormatter'; import { applyEdits } from 'vs/base/common/jsonEdit'; +import { compare } from 'vs/base/common/strings'; interface IExtensionsSyncPreviewResult extends ISyncPreviewResult { readonly localExtensions: ISyncExtension[]; @@ -35,8 +36,10 @@ interface ILastSyncUserData extends IRemoteUserData { skippedExtensions: ISyncExtension[] | undefined; } + export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { + private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/current.json` }); protected readonly version: number = 2; protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); } @@ -132,28 +135,49 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async stop(): Promise { } async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { - return [{ resource: joinPath(uri, 'extensions.json') }]; + return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }]; } async resolveContent(uri: URI): Promise { + if (isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) { + const localExtensions = await this.getLocalExtensions(); + return this.format(localExtensions); + } + let content = await super.resolveContent(uri); if (content) { return content; } + content = await super.resolveContent(dirname(uri)); if (content) { const syncData = this.parseSyncData(content); if (syncData) { switch (basename(uri)) { case 'extensions.json': - const edits = format(syncData.content, undefined, {}); - return applyEdits(syncData.content, edits); + return this.format(this.parseExtensions(syncData)); } } } + return null; } + private format(extensions: ISyncExtension[]): string { + extensions.sort((e1, e2) => { + if (!e1.identifier.uuid && e2.identifier.uuid) { + return -1; + } + if (e1.identifier.uuid && !e2.identifier.uuid) { + return 1; + } + return compare(e1.identifier.id, e2.identifier.id); + }); + const content = JSON.stringify(extensions); + const edits = format(content, undefined, {}); + return applyEdits(content, edits); + } + async acceptConflict(conflict: URI, content: string): Promise { throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`); } @@ -216,9 +240,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } if (hasLocalChanged) { - // back up all disabled or market place extensions - const backUpExtensions = localExtensions.filter(e => e.disabled || !!e.identifier.uuid); - await this.backupLocal(JSON.stringify(backUpExtensions)); + await this.backupLocal(JSON.stringify(localExtensions)); skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions); } diff --git a/src/vs/platform/userDataSync/common/globalStateMerge.ts b/src/vs/platform/userDataSync/common/globalStateMerge.ts index 4d4755fb245..9b7d47a307c 100644 --- a/src/vs/platform/userDataSync/common/globalStateMerge.ts +++ b/src/vs/platform/userDataSync/common/globalStateMerge.ts @@ -18,7 +18,7 @@ export interface IMergeResult { export function merge(localStorage: IStringDictionary, remoteStorage: IStringDictionary | null, baseStorage: IStringDictionary | null, storageKeys: ReadonlyArray, previouslySkipped: string[], logService: ILogService): IMergeResult { if (!remoteStorage) { - return { remote: localStorage, local: { added: {}, removed: [], updated: {} }, skipped: [] }; + return { remote: Object.keys(localStorage).length > 0 ? localStorage : null, local: { added: {}, removed: [], updated: {} }, skipped: [] }; } const localToRemote = compare(localStorage, remoteStorage); diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index aad86c57f60..ca00b220b5b 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { dirname, joinPath, basename } from 'vs/base/common/resources'; +import { dirname, joinPath, basename, isEqual } from 'vs/base/common/resources'; import { IFileService } from 'vs/platform/files/common/files'; import { IStringDictionary } from 'vs/base/common/collections'; import { edit } from 'vs/platform/userDataSync/common/content'; @@ -41,6 +41,7 @@ interface ILastSyncUserData extends IRemoteUserData { export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { + private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/current.json` }); protected readonly version: number = 1; constructor( @@ -139,28 +140,44 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs async stop(): Promise { } async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { - return [{ resource: joinPath(uri, 'globalState.json') }]; + return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }]; } async resolveContent(uri: URI): Promise { + if (isEqual(uri, GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI)) { + const localGlobalState = await this.getLocalGlobalState(); + return this.format(localGlobalState); + } + let content = await super.resolveContent(uri); if (content) { return content; } + content = await super.resolveContent(dirname(uri)); if (content) { const syncData = this.parseSyncData(content); if (syncData) { switch (basename(uri)) { case 'globalState.json': - const edits = format(syncData.content, undefined, {}); - return applyEdits(syncData.content, edits); + return this.format(JSON.parse(syncData.content)); } } } + return null; } + private format(globalState: IGlobalState): string { + const storageKeys = Object.keys(globalState.storage).sort(); + const storage: IStringDictionary = {}; + storageKeys.forEach(key => storage[key] = globalState.storage[key]); + globalState.storage = storage; + const content = JSON.stringify(globalState); + const edits = format(content, undefined, {}); + return applyEdits(content, edits); + } + async acceptConflict(conflict: URI, content: string): Promise { throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`); } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index ed6943af053..9d3c69444c9 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -248,10 +248,10 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`); } - if (lastSyncUserData?.ref !== remoteUserData.ref && (content !== null || fileContent !== null)) { + if (lastSyncUserData?.ref !== remoteUserData.ref) { this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`); - const lastSyncContent = this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null); - await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: { version: remoteUserData.syncData!.version, content: lastSyncContent } }); + const lastSyncContent = content !== null || fileContent !== null ? this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null) : null; + await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { version: remoteUserData.syncData!.version, content: lastSyncContent } : null }); this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`); } @@ -315,7 +315,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts }; } - private getKeybindingsContentFromSyncContent(syncContent: string): string | null { + getKeybindingsContentFromSyncContent(syncContent: string): string | null { try { const parsed = JSON.parse(syncContent); if (!this.configurationService.getValue('sync.keybindingsPerPlatform')) { diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index ce6c56c87b5..33b7eae171a 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -357,7 +357,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { return remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null; } - private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { + parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { try { const parsed = JSON.parse(syncContent); return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent }; diff --git a/src/vs/platform/userDataSync/common/snippetsMerge.ts b/src/vs/platform/userDataSync/common/snippetsMerge.ts index 42a9dfaae05..07c944c53cd 100644 --- a/src/vs/platform/userDataSync/common/snippetsMerge.ts +++ b/src/vs/platform/userDataSync/common/snippetsMerge.ts @@ -26,7 +26,7 @@ export function merge(local: IStringDictionary, remote: IStringDictionar removed: values(removed), updated, conflicts: [], - remote: local + remote: Object.keys(local).length > 0 ? local : null }; } diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index d802e543cf4..b8d4a194b8c 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -285,7 +285,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD private async doGeneratePreview(local: IStringDictionary, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary = {}, token: CancellationToken = CancellationToken.None): Promise { const localSnippets = this.toSnippetsContents(local); const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; - const lastSyncSnippets: IStringDictionary | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null; + const lastSyncSnippets: IStringDictionary | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null; if (remoteSnippets) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index eec0c4502a1..e158e4dce61 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -274,7 +274,7 @@ export interface IUserDataSynchroniser { pull(): Promise; push(): Promise; - sync(ref?: string): Promise; + sync(manifest: IUserDataManifest | null): Promise; stop(): Promise; getSyncPreview(): Promise diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 1c1bd056006..bd150093aab 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -136,7 +136,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ for (const synchroniser of this.synchronisers) { try { - await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resource] : undefined); + await synchroniser.sync(manifest); } catch (e) { this.handleSyncError(e, synchroniser.resource); this._syncErrors.push([synchroniser.resource, UserDataSyncError.toUserDataSyncError(e)]); diff --git a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts index 9f86b9354a5..7a1f331d5fa 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts @@ -44,11 +44,58 @@ suite('GlobalStateSync', () => { teardown(() => disposableStore.clear()); + test('when global state does not exist', async () => { + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when global state is created after first sync', async () => { + await testObject.sync(await testClient.manifest()); + updateStorage('a', 'value1', testClient); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.deepEqual(JSON.parse(lastSyncUserData!.syncData!.content).storage, { 'a': { version: 1, value: 'value1' } }); + }); + test('first time sync - outgoing to server (no state)', async () => { updateStorage('a', 'value1', testClient); await updateLocale(testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -63,7 +110,7 @@ suite('GlobalStateSync', () => { await updateLocale(client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -76,7 +123,7 @@ suite('GlobalStateSync', () => { await client2.sync(); updateStorage('b', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -94,7 +141,7 @@ suite('GlobalStateSync', () => { await client2.sync(); updateStorage('a', 'value2', client2); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -109,10 +156,10 @@ suite('GlobalStateSync', () => { test('sync adding a storage value', async () => { updateStorage('a', 'value1', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); updateStorage('b', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -127,10 +174,10 @@ suite('GlobalStateSync', () => { test('sync updating a storage value', async () => { updateStorage('a', 'value1', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); updateStorage('a', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -145,10 +192,10 @@ suite('GlobalStateSync', () => { test('sync removing a storage value', async () => { updateStorage('a', 'value1', testClient); updateStorage('b', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); removeStorage('b', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts new file mode 100644 index 00000000000..9fd790befce --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; +import { VSBuffer } from 'vs/base/common/buffer'; + +suite('KeybindingsSync', () => { + + const disposableStore = new DisposableStore(); + const server = new UserDataSyncTestServer(); + let client: UserDataSyncClient; + + let testObject: KeybindingsSynchroniser; + + setup(async () => { + client = disposableStore.add(new UserDataSyncClient(server)); + await client.setUp(true); + testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Keybindings) as KeybindingsSynchroniser; + disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear())); + }); + + teardown(() => disposableStore.clear()); + + test('when keybindings file does not exist', async () => { + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + assert.ok(!await fileService.exists(keybindingsResource)); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when keybindings file is created after first sync', async () => { + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + await testObject.sync(await client.manifest()); + await fileService.createFile(keybindingsResource, VSBuffer.fromString('[]')); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), '[]'); + }); + +}); diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index c0f87712cf1..a9e62326673 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -43,13 +43,67 @@ suite('SettingsSync', () => { setup(async () => { client = disposableStore.add(new UserDataSyncClient(server)); - await client.setUp(); + await client.setUp(true); testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Settings) as SettingsSynchroniser; disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear())); }); teardown(() => disposableStore.clear()); + test('when settings file does not exist', async () => { + const fileService = client.instantiationService.get(IFileService); + const settingResource = client.instantiationService.get(IEnvironmentService).settingsResource; + + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + assert.ok(!await fileService.exists(settingResource)); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when settings file is created after first sync', async () => { + const fileService = client.instantiationService.get(IFileService); + + const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource; + await testObject.sync(await client.manifest()); + await fileService.createFile(settingsResource, VSBuffer.fromString('{}')); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}'); + }); + test('sync for first time to the server', async () => { const expected = `{ @@ -75,7 +129,7 @@ suite('SettingsSync', () => { }`; await updateSettings(expected); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -99,7 +153,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -130,7 +184,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -161,7 +215,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -185,7 +239,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -203,7 +257,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -237,7 +291,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -285,7 +339,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -333,7 +387,7 @@ suite('SettingsSync', () => { await updateSettings(expected); try { - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.fail('should fail with invalid content error'); } catch (e) { assert.ok(e instanceof UserDataSyncError); diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index 7943e35359f..87da32e3a01 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -167,11 +167,62 @@ suite('SnippetsSync', () => { teardown(() => disposableStore.clear()); + test('when snippets does not exist', async () => { + const fileService = testClient.instantiationService.get(IFileService); + const snippetsResource = testClient.instantiationService.get(IEnvironmentService).snippetsHome; + + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + assert.ok(!await fileService.exists(snippetsResource)); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when snippet is created after first sync', async () => { + await testObject.sync(await testClient.manifest()); + await updateSnippet('html.json', htmlSnippet1, testClient); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.deepEqual(lastSyncUserData!.syncData!.content, JSON.stringify({ 'html.json': htmlSnippet1 })); + }); + test('first time sync - outgoing to server (no snippets)', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -186,7 +237,7 @@ suite('SnippetsSync', () => { await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -201,7 +252,7 @@ suite('SnippetsSync', () => { await client2.sync(); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -221,7 +272,7 @@ suite('SnippetsSync', () => { await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); @@ -234,7 +285,7 @@ suite('SnippetsSync', () => { await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); const conflicts = testObject.conflicts; await testObject.acceptConflict(conflicts[0].local, htmlSnippet1); @@ -259,7 +310,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); @@ -278,7 +329,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); let conflicts = testObject.conflicts; await testObject.acceptConflict(conflicts[0].local, htmlSnippet2); @@ -299,7 +350,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); const conflicts = testObject.conflicts; await testObject.acceptConflict(conflicts[0].local, htmlSnippet2); @@ -324,10 +375,10 @@ suite('SnippetsSync', () => { test('sync adding a snippet', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -345,12 +396,12 @@ suite('SnippetsSync', () => { test('sync adding a snippet - accept', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -362,10 +413,10 @@ suite('SnippetsSync', () => { test('sync updating a snippet', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -381,12 +432,12 @@ suite('SnippetsSync', () => { test('sync updating a snippet - accept', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -397,13 +448,13 @@ suite('SnippetsSync', () => { test('sync updating a snippet - conflict', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet3, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'); @@ -413,13 +464,13 @@ suite('SnippetsSync', () => { test('sync updating a snippet - resolve conflict', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet3, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet2); assert.equal(testObject.status, SyncStatus.Idle); @@ -437,10 +488,10 @@ suite('SnippetsSync', () => { test('sync removing a snippet', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -459,12 +510,12 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -478,13 +529,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); await removeSnippet('html.json', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -499,13 +550,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); @@ -517,13 +568,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet3); assert.equal(testObject.status, SyncStatus.Idle); @@ -544,13 +595,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await testObject.acceptConflict(testObject.conflicts[0].local, ''); assert.equal(testObject.status, SyncStatus.Idle); @@ -601,7 +652,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -622,7 +673,7 @@ suite('SnippetsSync', () => { await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 3c2583e4626..340b95c0f24 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -79,7 +79,7 @@ suite('TestSynchronizer', () => { const promise = Event.toPromise(testObject.onDoSyncCall.event); - testObject.sync(); + testObject.sync(await client.manifest()); await promise; assert.deepEqual(actual, [SyncStatus.Syncing]); @@ -94,7 +94,7 @@ suite('TestSynchronizer', () => { const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]); assert.deepEqual(testObject.status, SyncStatus.Idle); @@ -107,7 +107,7 @@ suite('TestSynchronizer', () => { const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.HasConflicts]); assert.deepEqual(testObject.status, SyncStatus.HasConflicts); @@ -122,7 +122,7 @@ suite('TestSynchronizer', () => { disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); try { - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.fail('Should fail'); } catch (e) { assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]); @@ -134,12 +134,12 @@ suite('TestSynchronizer', () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); const promise = Event.toPromise(testObject.onDoSyncCall.event); - testObject.sync(); + testObject.sync(await client.manifest()); await promise; const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, []); assert.deepEqual(testObject.status, SyncStatus.Syncing); @@ -154,7 +154,7 @@ suite('TestSynchronizer', () => { const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, []); assert.deepEqual(testObject.status, SyncStatus.Idle); @@ -164,11 +164,11 @@ suite('TestSynchronizer', () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { status: SyncStatus.HasConflicts }; testObject.syncBarrier.open(); - await testObject.sync(); + await testObject.sync(await client.manifest()); const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, []); assert.deepEqual(testObject.status, SyncStatus.HasConflicts); @@ -178,7 +178,7 @@ suite('TestSynchronizer', () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); // Sync once testObject.syncBarrier.open(); - await testObject.sync(); + await testObject.sync(await client.manifest()); testObject.syncBarrier = new Barrier(); // update remote data before syncing so that 412 is thrown by server @@ -190,8 +190,9 @@ suite('TestSynchronizer', () => { }); // Start sycing - const { ref } = await userDataSyncStoreService.read(testObject.resource, null); - await testObject.sync(ref); + const manifest = await client.manifest(); + const ref = manifest!.latest![testObject.resource]; + await testObject.sync(await client.manifest()); assert.deepEqual(server.requests, [ { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': ref } }, diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index d9168c084cc..ae2edfc85d1 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -124,6 +124,10 @@ export class UserDataSyncClient extends Disposable { return this.instantiationService.get(IUserDataSyncStoreService).read(resource, null); } + manifest(): Promise { + return this.instantiationService.get(IUserDataSyncStoreService).manifest(); + } + } export class UserDataSyncTestServer implements IRequestService { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 5c2b51c2840..b7fbb82ed1c 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -45,7 +45,6 @@ suite('UserDataSyncService', () => { { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, ]); @@ -71,13 +70,10 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, // Snippets { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, ]); @@ -384,7 +380,6 @@ suite('UserDataSyncService', () => { { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, ]); diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 0db6846abd9..5c6facab0a9 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -105,7 +105,7 @@ export interface IWindowsMainService { readonly onWindowsCountChanged: Event; open(openConfig: IOpenConfiguration): ICodeWindow[]; - openEmptyWindow(context: OpenContext, options?: IOpenEmptyWindowOptions): ICodeWindow[]; + openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[]; openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[]; sendToFocused(channel: string, ...args: any[]): void; @@ -118,9 +118,12 @@ export interface IWindowsMainService { getWindowCount(): number; } -export interface IOpenConfiguration { +export interface IBaseOpenConfiguration { readonly context: OpenContext; readonly contextWindowId?: number; +} + +export interface IOpenConfiguration extends IBaseOpenConfiguration { readonly cli: ParsedArgs; readonly userEnv?: IProcessEnvironment; readonly urisToOpen?: IWindowOpenable[]; @@ -136,3 +139,5 @@ export interface IOpenConfiguration { readonly initialStartup?: boolean; readonly noRecentEntry?: boolean; } + +export interface IOpenEmptyConfiguration extends IBaseOpenConfiguration { } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 266c235a0fe..cdd6731748d 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -24,7 +24,7 @@ import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, INativeWindowConfiguration, OpenContext, IAddFoldersRequest, IPathsToWaitFor } from 'vs/platform/windows/node/window'; import { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; -import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; import { IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension, IRecent } from 'vs/platform/workspaces/common/workspaces'; @@ -393,7 +393,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }; } - openEmptyWindow(context: OpenContext, options?: IOpenEmptyWindowOptions): ICodeWindow[] { + openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[] { let cli = this.environmentService.args; const remote = options?.remoteAuthority; if (cli && (cli.remote !== remote)) { @@ -403,7 +403,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const forceReuseWindow = options?.forceReuseWindow; const forceNewWindow = !forceReuseWindow; - return this.open({ context, cli, forceEmpty: true, forceNewWindow, forceReuseWindow }); + return this.open({ ...openConfig, cli, forceEmpty: true, forceNewWindow, forceReuseWindow }); } open(openConfig: IOpenConfiguration): ICodeWindow[] { @@ -474,7 +474,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Make sure to pass focus to the most relevant of the windows if we open multiple if (usedWindows.length > 1) { - const focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); let focusLastOpened = true; let focusLastWindow = true; @@ -753,15 +752,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const remoteAuthority = fileInputs ? fileInputs.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined); for (let i = 0; i < emptyToOpen; i++) { - usedWindows.push(this.openInBrowserWindow({ - userEnv: openConfig.userEnv, - cli: openConfig.cli, - initialStartup: openConfig.initialStartup, - remoteAuthority, - forceNewWindow: openFolderInNewWindow, - forceNewTabbedWindow: openConfig.forceNewTabbedWindow, - fileInputs - })); + usedWindows.push(this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, fileInputs)); // Reset these because we handled them fileInputs = undefined; @@ -801,12 +792,29 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return window; } + private doOpenEmpty(openConfig: IOpenConfiguration, forceNewWindow: boolean, remoteAuthority: string | undefined, fileInputs: IFileInputs | undefined, windowToUse?: ICodeWindow): ICodeWindow { + if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') { + windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/microsoft/vscode/issues/97172 + } + + return this.openInBrowserWindow({ + userEnv: openConfig.userEnv, + cli: openConfig.cli, + initialStartup: openConfig.initialStartup, + remoteAuthority, + forceNewWindow, + forceNewTabbedWindow: openConfig.forceNewTabbedWindow, + fileInputs, + windowToUse + }); + } + private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IPathToOpen, forceNewWindow: boolean, fileInputs: IFileInputs | undefined, windowToUse?: ICodeWindow): ICodeWindow { if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') { windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/Microsoft/vscode/issues/49587 } - const browserWindow = this.openInBrowserWindow({ + return this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, @@ -818,8 +826,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic forceNewTabbedWindow: openConfig.forceNewTabbedWindow, windowToUse }); - - return browserWindow; } private getPathsToOpen(openConfig: IOpenConfiguration): IPathToOpen[] { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 6b7f9014818..d412b4e8a56 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2270,7 +2270,7 @@ declare module 'vscode' { * A code lens provider adds [commands](#Command) to source text. The commands will be shown * as dedicated horizontal lines in between the source text. */ - export interface CodeLensProvider { + export interface CodeLensProvider { /** * An optional event to signal that the code lenses from this provider have changed. @@ -2287,17 +2287,17 @@ declare module 'vscode' { * @return An array of code lenses or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ - provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult; + provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult; /** * This function will be called for each visible code lens, usually when scrolling and after * calls to [compute](#CodeLensProvider.provideCodeLenses)-lenses. * - * @param codeLens code lens that must be resolved. + * @param codeLens Code lens that must be resolved. * @param token A cancellation token. * @return The given, resolved code lens or thenable that resolves to such. */ - resolveCodeLens?(codeLens: CodeLens, token: CancellationToken): ProviderResult; + resolveCodeLens?(codeLens: T, token: CancellationToken): ProviderResult; } /** @@ -2798,7 +2798,7 @@ declare module 'vscode' { * The workspace symbol provider interface defines the contract between extensions and * the [symbol search](https://code.visualstudio.com/docs/editor/editingevolved#_open-symbol-by-name)-feature. */ - export interface WorkspaceSymbolProvider { + export interface WorkspaceSymbolProvider { /** * Project-wide search for a symbol matching the given query string. @@ -2817,7 +2817,7 @@ declare module 'vscode' { * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ - provideWorkspaceSymbols(query: string, token: CancellationToken): ProviderResult; + provideWorkspaceSymbols(query: string, token: CancellationToken): ProviderResult; /** * Given a symbol fill in its [location](#SymbolInformation.location). This method is called whenever a symbol @@ -2831,7 +2831,7 @@ declare module 'vscode' { * @return The resolved symbol or a thenable that resolves to that. When no result is returned, * the given `symbol` is used. */ - resolveWorkspaceSymbol?(symbol: SymbolInformation, token: CancellationToken): ProviderResult; + resolveWorkspaceSymbol?(symbol: T, token: CancellationToken): ProviderResult; } /** @@ -3858,7 +3858,7 @@ declare module 'vscode' { * Represents a collection of [completion items](#CompletionItem) to be presented * in the editor. */ - export class CompletionList { + export class CompletionList { /** * This list is not complete. Further typing should result in recomputing @@ -3869,7 +3869,7 @@ declare module 'vscode' { /** * The completion items. */ - items: CompletionItem[]; + items: T[]; /** * Creates a new completion list. @@ -3877,7 +3877,7 @@ declare module 'vscode' { * @param items The completion items. * @param isIncomplete The list is not complete. */ - constructor(items?: CompletionItem[], isIncomplete?: boolean); + constructor(items?: T[], isIncomplete?: boolean); } /** @@ -3931,7 +3931,7 @@ declare module 'vscode' { * Providers are asked for completions either explicitly by a user gesture or -depending on the configuration- * implicitly when typing words or trigger characters. */ - export interface CompletionItemProvider { + export interface CompletionItemProvider { /** * Provide completion items for the given position and document. @@ -3944,7 +3944,7 @@ declare module 'vscode' { * @return An array of completions, a [completion list](#CompletionList), or a thenable that resolves to either. * The lack of a result can be signaled by returning `undefined`, `null`, or an empty array. */ - provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult; + provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult>; /** * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) @@ -3961,7 +3961,7 @@ declare module 'vscode' { * @return The resolved completion item or a thenable that resolves to of such. It is OK to return the given * `item`. When no result is returned, the given `item` will be used. */ - resolveCompletionItem?(item: CompletionItem, token: CancellationToken): ProviderResult; + resolveCompletionItem?(item: T, token: CancellationToken): ProviderResult; } @@ -4003,7 +4003,7 @@ declare module 'vscode' { * The document link provider defines the contract between extensions and feature of showing * links in the editor. */ - export interface DocumentLinkProvider { + export interface DocumentLinkProvider { /** * Provide links for the given document. Note that the editor ships with a default provider that detects @@ -4014,7 +4014,7 @@ declare module 'vscode' { * @return An array of [document links](#DocumentLink) or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ - provideDocumentLinks(document: TextDocument, token: CancellationToken): ProviderResult; + provideDocumentLinks(document: TextDocument, token: CancellationToken): ProviderResult; /** * Given a link fill in its [target](#DocumentLink.target). This method is called when an incomplete @@ -4025,7 +4025,7 @@ declare module 'vscode' { * @param link The link that is to be resolved. * @param token A cancellation token. */ - resolveDocumentLink?(link: DocumentLink, token: CancellationToken): ProviderResult; + resolveDocumentLink?(link: T, token: CancellationToken): ProviderResult; } /** @@ -6036,13 +6036,13 @@ declare module 'vscode' { * A task provider allows to add tasks to the task service. * A task provider is registered via #tasks.registerTaskProvider. */ - export interface TaskProvider { + export interface TaskProvider { /** * Provides tasks. * @param token A cancellation token. * @return an array of tasks */ - provideTasks(token?: CancellationToken): ProviderResult; + provideTasks(token?: CancellationToken): ProviderResult; /** * Resolves a task that has no [`execution`](#Task.execution) set. Tasks are @@ -6057,7 +6057,7 @@ declare module 'vscode' { * @param token A cancellation token. * @return The resolved task */ - resolveTask(task: Task, token?: CancellationToken): ProviderResult; + resolveTask(task: T, token?: CancellationToken): ProviderResult; } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index da705cb2bb8..5eda9075136 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -77,6 +77,11 @@ declare module 'vscode' { readonly id: string; readonly displayName: string; + /** + * Whether it is possible to be signed into multiple accounts at once. + */ + supportsMultipleAccounts: boolean; + /** * An [event](#Event) which fires when the array of sessions has changed, or data * within a session has changed. @@ -109,6 +114,41 @@ declare module 'vscode' { export const providerIds: string[]; /** + * Returns whether a provider has any sessions matching the requested scopes. This request + * is transparent to the user, not UI is shown. Rejects if a provider with providerId is not + * registered. + * @param providerId The id of the provider + * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication + * provider + */ + export function hasSessions(providerId: string, scopes: string[]): Thenable; + + export interface GetSessionOptions { + /** + * Whether login should be performed if there is no matching session. Defaults to false. + */ + createIfNone?: boolean; + + /** + * Whether the existing user session preference should be cleared. Set to allow the user to switch accounts. + * Defaults to false. + */ + clearSessionPreference?: boolean; + } + + /** + * Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not + * registered, or if the user does not consent to sharing authentication information with + * the extension. If there are multiple sessions with the same scopes, the user will be shown a + * quickpick to select which account they would like to use. + * @param providerId The id of the provider to use + * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider + * @param options The [getSessionOptions](#GetSessionOptions) to use + */ + export function getSession(providerId: string, scopes: string[], options: GetSessionOptions): Thenable; + + /** + * @deprecated * Get existing authentication sessions. Rejects if a provider with providerId is not * registered, or if the user does not consent to sharing authentication information with * the extension. @@ -116,9 +156,10 @@ declare module 'vscode' { * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication * provider */ - export function getSessions(providerId: string, scopes: string[]): Thenable; + export function getSessions(providerId: string, scopes: string[]): Thenable>; /** + * @deprecated * Prompt a user to login to create a new authenticaiton session. Rejects if a provider with * providerId is not registered, or if the user does not consent to sharing authentication * information with the extension. @@ -129,6 +170,7 @@ declare module 'vscode' { export function login(providerId: string, scopes: string[]): Thenable; /** + * @deprecated * Logout of a specific session. * @param providerId The id of the provider to use * @param sessionId The session id to remove @@ -1640,12 +1682,6 @@ declare module 'vscode' { edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable; } - export interface NotebookProvider { - resolveNotebook(editor: NotebookEditor): Promise; - executeCell(document: NotebookDocument, cell: NotebookCell | undefined, token: CancellationToken): Promise; - save(document: NotebookDocument): Promise; - } - export interface NotebookOutputSelector { type: string; subTypes?: string[]; @@ -1688,11 +1724,20 @@ declare module 'vscode' { readonly metadata: NotebookDocumentMetadata; } + interface NotebookDocumentEditEvent { + + /** + * The document that the edit is for. + */ + readonly document: NotebookDocument; + } + export interface NotebookContentProvider { openNotebook(uri: Uri): NotebookData | Promise; saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise; saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; - readonly onDidChangeNotebook: Event; + readonly onDidChangeNotebook: Event; + // revert?(document: NotebookDocument, cancellation: CancellationToken): Thenable; // backup?(document: NotebookDocument, cancellation: CancellationToken): Thenable; @@ -1708,11 +1753,6 @@ declare module 'vscode' { provider: NotebookContentProvider ): Disposable; - export function registerNotebookProvider( - notebookType: string, - provider: NotebookProvider - ): Disposable; - export function registerNotebookOutputRenderer( type: string, outputSelector: NotebookOutputSelector, diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index ccabaa3af36..1ae18d4aee4 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -294,7 +294,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, - @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); @@ -314,6 +315,72 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this.authenticationService.sessionsUpdate(id, event); } + async $getSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise { + if (!potentialSessions.length) { + throw new Error('No potential sessions found'); + } + + if (clearSessionPreference) { + this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.GLOBAL); + } else { + const existingSessionPreference = this.storageService.get(`${extensionName}-${providerId}`, StorageScope.GLOBAL); + if (existingSessionPreference) { + const matchingSession = potentialSessions.find(session => session.id === existingSessionPreference); + if (matchingSession) { + const allowed = await this.$getSessionsPrompt(providerId, matchingSession.account.displayName, providerName, extensionId, extensionName); + if (allowed) { + return matchingSession; + } + } + } + } + + return new Promise((resolve, reject) => { + const quickPick = this.quickInputService.createQuickPick<{ label: string, session?: modes.AuthenticationSession }>(); + quickPick.ignoreFocusOut = true; + const items: { label: string, session?: modes.AuthenticationSession }[] = potentialSessions.map(session => { + return { + label: session.account.displayName, + session + }; + }); + + items.push({ + label: nls.localize('useOtherAccount', "Sign in to another account") + }); + + quickPick.items = items; + quickPick.title = nls.localize('selectAccount', "The extension '{0}' wants to access a {1} account", extensionName, providerName); + quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName); + + quickPick.onDidAccept(async _ => { + const selected = quickPick.selectedItems[0]; + + const session = selected.session ?? await this.authenticationService.login(providerId, scopes); + + const accountName = session.account.displayName; + const allowList = readAllowedExtensions(this.storageService, providerId, accountName); + allowList.push({ id: extensionId, name: extensionName }); + this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL); + + this.storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.GLOBAL); + + quickPick.dispose(); + resolve(session); + }); + + quickPick.onDidHide(_ => { + if (!quickPick.selectedItems[0]) { + reject('User did not consent to account access'); + } + + quickPick.dispose(); + }); + + quickPick.show(); + }); + } + async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise { addAccountUsage(providerId, accountName, extensionName); diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 4653ef3ffec..efdef129e3f 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -8,7 +8,7 @@ import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IEx import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, CellKind, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -84,7 +84,9 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo registerListeners() { this._register(this._notebookService.onDidChangeActiveEditor(e => { - this._proxy.$updateActiveEditor(e.viewType, e.uri); + this._proxy.$acceptDocumentAndEditorsDelta({ + newActiveEditor: e.uri + }); })); const updateOrder = () => { @@ -129,16 +131,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return; } - async $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise { - let controller = this._notebookProviders.get(viewType); - - if (controller) { - controller.createNotebookDocument(handle, viewType, resource); - } - - return; - } - async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise { let controller = this._notebookProviders.get(viewType); @@ -163,11 +155,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } } - async resolveNotebook(viewType: string, uri: URI): Promise { - let handle = await this._proxy.$resolveNotebook(viewType, uri); - return handle; - } - async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { let controller = this._notebookProviders.get(viewType); controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers); @@ -195,6 +182,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo export class MainThreadNotebookController implements IMainNotebookController { private _mapping: Map = new Map(); + static documentHandle: number = 0; constructor( private readonly _proxy: ExtHostNotebookShape, @@ -203,26 +191,44 @@ export class MainThreadNotebookController implements IMainNotebookController { ) { } - async resolveNotebook(viewType: string, uri: URI): Promise { - // TODO: resolve notebook should wait for all notebook document destory operations to finish. + async createNotebook(viewType: string, uri: URI, forBackup: boolean, forceReload: boolean): Promise { let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); if (mainthreadNotebook) { + if (forceReload) { + const data = await this._proxy.$resolveNotebookData(viewType, uri); + if (!data) { + return; + } + + mainthreadNotebook.textModel.languages = data.languages; + mainthreadNotebook.textModel.metadata = data.metadata; + mainthreadNotebook.textModel.applyEdit(mainthreadNotebook.textModel.versionId, [ + { editType: CellEditType.Delete, count: mainthreadNotebook.textModel.cells.length, index: 0 }, + { editType: CellEditType.Insert, index: 0, cells: data.cells } + ]); + } return mainthreadNotebook.textModel; } - let notebookHandle = await this._mainThreadNotebook.resolveNotebook(viewType, uri); - if (notebookHandle !== undefined) { - mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); - if (mainthreadNotebook && mainthreadNotebook.textModel.cells.length === 0) { - // it's empty, we should create an empty template one - const mainCell = mainthreadNotebook.textModel.createCellTextModel([''], mainthreadNotebook.textModel.languages.length ? mainthreadNotebook.textModel.languages[0] : '', CellKind.Code, [], undefined); - mainthreadNotebook.textModel.insertTemplateCell(mainCell); - } - return mainthreadNotebook?.textModel; + let document = new MainThreadNotebookDocument(this._proxy, MainThreadNotebookController.documentHandle++, viewType, uri); + await this.createNotebookDocument(document); + + if (forBackup) { + return document.textModel; } - return undefined; + // open notebook document + const data = await this._proxy.$resolveNotebookData(viewType, uri); + if (!data) { + return; + } + + document.textModel.languages = data.languages; + document.textModel.metadata = data.metadata; + document.textModel.initialize(data!.cells); + + return document.textModel; } async tryApplyEdits(resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise { @@ -250,12 +256,32 @@ export class MainThreadNotebookController implements IMainNotebookController { this._proxy.$onDidReceiveMessage(uri, message); } - // Methods for ExtHost - async createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise { - let document = new MainThreadNotebookDocument(this._proxy, handle, viewType, URI.revive(resource)); - this._mapping.set(URI.revive(resource).toString(), document); + async createNotebookDocument(document: MainThreadNotebookDocument): Promise { + this._mapping.set(document.uri.toString(), document); + + await this._proxy.$acceptDocumentAndEditorsDelta({ + addedDocuments: [{ + viewType: document.viewType, + handle: document.handle, + uri: document.uri + }] + }); } + async removeNotebookDocument(notebook: INotebookTextModel): Promise { + let document = this._mapping.get(URI.from(notebook.uri).toString()); + + if (!document) { + return; + } + + await this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [notebook.uri] }); + document.dispose(); + this._mapping.delete(URI.from(notebook.uri).toString()); + } + + // Methods for ExtHost + updateLanguages(resource: UriComponents, languages: string[]) { let document = this._mapping.get(URI.from(resource).toString()); document?.textModel.updateLanguages(languages); @@ -280,21 +306,12 @@ export class MainThreadNotebookController implements IMainNotebookController { return this._proxy.$executeNotebook(this._viewType, uri, handle, token); } - async destoryNotebookDocument(notebook: INotebookTextModel): Promise { - let document = this._mapping.get(URI.from(notebook.uri).toString()); - - if (!document) { - return; - } - - let removeFromExtHost = await this._proxy.$destoryNotebookDocument(this._viewType, notebook.uri); - if (removeFromExtHost) { - document.dispose(); - this._mapping.delete(URI.from(notebook.uri).toString()); - } - } - async save(uri: URI, token: CancellationToken): Promise { return this._proxy.$saveNotebook(this._viewType, uri, token); } + + async saveAs(uri: URI, target: URI, token: CancellationToken): Promise { + return this._proxy.$saveNotebookAs(this._viewType, uri, target, token); + + } } diff --git a/src/vs/workbench/api/browser/mainThreadTheming.ts b/src/vs/workbench/api/browser/mainThreadTheming.ts index 4f0fa417240..d810863f852 100644 --- a/src/vs/workbench/api/browser/mainThreadTheming.ts +++ b/src/vs/workbench/api/browser/mainThreadTheming.ts @@ -25,6 +25,7 @@ export class MainThreadTheming implements MainThreadThemingShape { this._themeChangeListener = this._themeService.onDidColorThemeChange(e => { this._proxy.$onColorThemeChange(this._themeService.getColorTheme().type); }); + this._proxy.$onColorThemeChange(this._themeService.getColorTheme().type); } dispose(): void { diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 66dd9fa03c8..9e4d58a2cd8 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -325,13 +325,14 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma throw new Error(`Provider for ${viewType} already registered`); } - this._customEditorService.registerCustomEditorCapabilities(viewType, { - supportsMultipleEditorsPerDocument - }); - const extension = reviveWebviewExtension(extensionData); const disposables = new DisposableStore(); + + disposables.add(this._customEditorService.registerCustomEditorCapabilities(viewType, { + supportsMultipleEditorsPerDocument + })); + disposables.add(this._webviewWorkbenchService.registerResolver({ canResolve: (webviewInput) => { return webviewInput instanceof CustomEditorInput && webviewInput.viewType === viewType; diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 46bf7934fd0..df17b498274 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -80,6 +80,9 @@ interface IUserFriendlyViewDescriptor { name: string; when?: string; + icon?: string; + contextualTitle?: string; + // From 'remoteViewDescriptor' type group?: string; remoteName?: string | string[]; @@ -100,6 +103,14 @@ const viewDescriptor: IJSONSchema = { description: localize('vscode.extension.contributes.view.when', 'Condition which must be true to show this view'), type: 'string' }, + icon: { + description: localize('vscode.extension.contributes.view.icon', "Path to the view icon. View icons are displayed when the name of the view cannot be shown. It is recommended that icons be in SVG, though any image file type is accepted."), + type: 'string' + }, + contextualTitle: { + description: localize('vscode.extension.contributes.view.contextualTitle', "Human-readable context for when the view is moved out of its original location. By default, the view's container name will be used. Will be shown"), + type: 'string' + }, } }; @@ -406,12 +417,14 @@ class ViewsExtensionHandler implements IWorkbenchContribution { ? container.viewOrderDelegate.getOrder(item.group) : undefined; + const icon = item.icon ? resources.joinPath(extension.description.extensionLocation, item.icon) : undefined; const viewDescriptor = { id: item.id, name: item.name, ctorDescriptor: new SyncDescriptor(TreeViewPane), when: ContextKeyExpr.deserialize(item.when), - containerIcon: viewContainer?.icon, + containerIcon: icon || viewContainer?.icon, + containerTitle: item.contextualTitle || viewContainer?.name, canToggleVisibility: true, canMoveView: true, treeView: this.instantiationService.createInstance(CustomTreeView, item.id, item.name), @@ -468,6 +481,14 @@ class ViewsExtensionHandler implements IWorkbenchContribution { collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when')); return false; } + if (descriptor.icon && typeof descriptor.icon !== 'string') { + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'icon')); + return false; + } + if (descriptor.contextualTitle && typeof descriptor.contextualTitle !== 'string') { + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'contextualTitle')); + return false; + } } return true; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a4736ede416..57de023e859 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -196,6 +196,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get providerIds(): string[] { return extHostAuthentication.providerIds; }, + hasSessions(providerId: string, scopes: string[]): Thenable { + return extHostAuthentication.hasSessions(providerId, scopes); + }, + getSession(providerId: string, scopes: string[], options: vscode.authentication.GetSessionOptions): Thenable { + return extHostAuthentication.getSession(extension, providerId, scopes, options); + }, getSessions(providerId: string, scopes: string[]): Thenable { return extHostAuthentication.getSessions(extension, providerId, scopes); }, @@ -910,10 +916,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidCloseNotebookDocument; }, - registerNotebookProvider: (viewType: string, provider: vscode.NotebookProvider) => { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookProvider(extension, viewType, provider); - }, registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider) => { checkProposedApiEnabled(extension); return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c6b3c31999d..316f06d40d8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -160,6 +160,7 @@ export interface MainThreadAuthenticationShape extends IDisposable { $registerAuthenticationProvider(id: string, displayName: string): void; $unregisterAuthenticationProvider(id: string): void; $onDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void; + $getSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise; $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise; $loginPrompt(providerName: string, extensionName: string): Promise; $setTrustedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise; @@ -691,7 +692,6 @@ export interface MainThreadNotebookShape extends IDisposable { $unregisterNotebookProvider(viewType: string): Promise; $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise; $unregisterNotebookRenderer(handle: number): Promise; - $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise; $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise; @@ -1538,16 +1538,31 @@ export interface INotebookEditorPropertiesChangeData { selections: INotebookSelectionChangeEvent | null; } +export interface INotebookModelAddedData { + uri: UriComponents; + handle: number; + // versionId: number; + viewType: string; +} + +export interface INotebookDocumentsAndEditorsDelta { + removedDocuments?: UriComponents[]; + addedDocuments?: INotebookModelAddedData[]; + // removedEditors?: string[]; + // addedEditors?: ITextEditorAddData[]; + newActiveEditor?: UriComponents | null; +} + export interface ExtHostNotebookShape { - $resolveNotebook(viewType: string, uri: UriComponents): Promise; + $resolveNotebookData(viewType: string, uri: UriComponents): Promise; $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise; $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise; - $updateActiveEditor(viewType: string, uri: UriComponents): Promise; - $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise; + $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; $onDidReceiveMessage(uri: UriComponents, message: any): void; $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void; $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void; + $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): Promise; } export interface ExtHostStorageShape { diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 888aefea967..1cab57a73f4 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -33,6 +33,56 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { return ids; } + async hasSessions(providerId: string, scopes: string[]): Promise { + const provider = this._authenticationProviders.get(providerId); + if (!provider) { + throw new Error(`No authentication provider with id '${providerId}' is currently registered.`); + } + + const orderedScopes = scopes.sort().join(' '); + return !!(await provider.getSessions()).filter(session => session.scopes.sort().join(' ') === orderedScopes).length; + } + + async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.authentication.GetSessionOptions): Promise { + const provider = this._authenticationProviders.get(providerId); + if (!provider) { + throw new Error(`No authentication provider with id '${providerId}' is currently registered.`); + } + + const orderedScopes = scopes.sort().join(' '); + const sessions = (await provider.getSessions()).filter(session => session.scopes.sort().join(' ') === orderedScopes); + if (sessions.length) { + const extensionName = requestingExtension.displayName || requestingExtension.name; + if (!provider.supportsMultipleAccounts) { + const session = sessions[0]; + const allowed = await this._proxy.$getSessionsPrompt(provider.id, session.account.displayName, provider.displayName, ExtensionIdentifier.toKey(requestingExtension.identifier), extensionName); + if (allowed) { + return session; + } else { + throw new Error('User did not consent to login.'); + } + } + + // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid + const selected = await this._proxy.$getSession(provider.id, provider.displayName, ExtensionIdentifier.toKey(requestingExtension.identifier), extensionName, sessions, scopes, !!options.clearSessionPreference); + return sessions.find(session => session.id === selected.id); + } else { + if (options.createIfNone) { + const extensionName = requestingExtension.displayName || requestingExtension.name; + const isAllowed = await this._proxy.$loginPrompt(provider.displayName, extensionName); + if (!isAllowed) { + throw new Error('User did not consent to login.'); + } + + const session = await provider.login(scopes); + await this._proxy.$setTrustedExtension(provider.id, session.account.displayName, ExtensionIdentifier.toKey(requestingExtension.identifier), extensionName); + return session; + } else { + return undefined; + } + } + } + async getSessions(requestingExtension: IExtensionDescription, providerId: string, scopes: string[]): Promise { const provider = this._authenticationProviders.get(providerId); if (!provider) { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 1b1d7db1f80..66dba087614 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -10,10 +10,10 @@ import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecyc import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape, INotebookEditorPropertiesChangeData } from 'vs/workbench/api/common/extHost.protocol'; +import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape, INotebookEditorPropertiesChangeData, INotebookDocumentsAndEditorsDelta } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Disposable as VSCodeDisposable } from './extHostTypes'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; @@ -345,8 +345,6 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo transformMimeTypes(output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto { let mimeTypes = Object.keys(output.data); - - // TODO@rebornix, the document display order might be assigned a bit later. We need to postpone sending the outputs to the core side. let coreDisplayOrder = this.renderingHandler.outputDisplayOrder; const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], this._displayOrder, coreDisplayOrder?.defaultOrder || []); @@ -415,7 +413,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo } } -export class NotebookEditorCellEdit { +export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellEdit { private _finalized: boolean = false; private readonly _documentVersionId: number; private _collectedEdits: ICellEditOperation[] = []; @@ -526,13 +524,13 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook })); } - edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable { - const edit = new NotebookEditorCellEdit(this); + edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { + const edit = new NotebookEditorCellEditBuilder(this); callback(edit); return this._applyEdit(edit); } - private _applyEdit(editBuilder: NotebookEditorCellEdit): Promise { + private _applyEdit(editBuilder: NotebookEditorCellEditBuilder): Promise { const editData = editBuilder.finalize(); // return when there is nothing to do @@ -625,7 +623,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private static _handlePool: number = 0; private readonly _proxy: MainThreadNotebookShape; - private readonly _notebookProviders = new Map(); private readonly _notebookContentProviders = new Map(); private readonly _documents = new Map(); private readonly _editors = new Map; }>(); @@ -706,31 +703,13 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return matches; } - registerNotebookProvider( - extension: IExtensionDescription, - viewType: string, - provider: vscode.NotebookProvider, - ): vscode.Disposable { - - if (this._notebookProviders.has(viewType)) { - throw new Error(`Notebook provider for '${viewType}' already registered`); - } - - this._notebookProviders.set(viewType, { extension, provider }); - this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType); - return new VSCodeDisposable(() => { - this._notebookProviders.delete(viewType); - this._proxy.$unregisterNotebookProvider(viewType); - }); - } - registerNotebookContentProvider( extension: IExtensionDescription, viewType: string, provider: vscode.NotebookContentProvider, ): vscode.Disposable { - if (this._notebookProviders.has(viewType)) { + if (this._notebookContentProviders.has(viewType)) { throw new Error(`Notebook provider for '${viewType}' already registered`); } @@ -742,97 +721,99 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); } - async _resolveNotebookFromContentProvider(viewType: string, uri: UriComponents): Promise { + async $resolveNotebookData(viewType: string, uri: UriComponents): Promise { let provider = this._notebookContentProviders.get(viewType); + let document = this._documents.get(URI.revive(uri).toString()); - if (provider) { - const revivedUri = URI.revive(uri); - if (!this._documents.has(revivedUri.toString())) { - let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, revivedUri, this); - await this._proxy.$createNotebookDocument( - document.handle, - viewType, - uri - ); + if (provider && document) { + const rawCells = await provider.provider.openNotebook(URI.revive(uri)); + const renderers = new Set(); + const dto = { + metadata: { + ...notebookDocumentMetadataDefaults, + ...rawCells.metadata + }, + languages: rawCells.languages, + cells: rawCells.cells.map(cell => { + let transformedOutputs = cell.outputs.map(output => { + if (output.outputKind === CellOutputKind.Rich) { + // TODO display string[] + const ret = this._transformMimeTypes(document!, (rawCells.metadata.displayOrder as string[]) || [], output); - this._documents.set(revivedUri.toString(), document); - } + if (ret.orderedMimeTypes[ret.pickedMimeTypeIndex].isResolved) { + renderers.add(ret.orderedMimeTypes[ret.pickedMimeTypeIndex].rendererId!); + } + return ret; + } else { + return output as IStreamOutput | IErrorOutput; + } + }); - const onDidReceiveMessage = new Emitter(); - - let editor = new ExtHostNotebookEditor( - viewType, - `${ExtHostNotebookController._handlePool++}`, - revivedUri, - this._proxy, - onDidReceiveMessage, - this._documents.get(revivedUri.toString())!, - this._documentsAndEditors - ); - - this._editors.set(revivedUri.toString(), { editor, onDidReceiveMessage }); - - const data = await provider.provider.openNotebook(revivedUri); - editor.document.languages = data.languages; - editor.document.metadata = { - ...notebookDocumentMetadataDefaults, - ...data.metadata + return { + language: cell.language, + cellKind: cell.cellKind, + metadata: cell.metadata, + source: cell.source, + outputs: transformedOutputs + }; + }) }; - await editor.edit(editBuilder => { - for (let i = 0; i < data.cells.length; i++) { - const cell = data.cells[i]; - editBuilder.insert(0, cell.source, cell.language, cell.cellKind, cell.outputs, cell.metadata); - } - }); - - this._onDidOpenNotebookDocument.fire(editor.document); - return editor.document.handle; - } else { - return Promise.resolve(undefined); + return dto; } + + return; } - async $resolveNotebook(viewType: string, uri: UriComponents): Promise { - let notebookFromNotebookContentProvider = await this._resolveNotebookFromContentProvider(viewType, uri); + private _transformMimeTypes(document: ExtHostNotebookDocument, displayOrder: string[], output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto { + let mimeTypes = Object.keys(output.data); + let coreDisplayOrder = this.outputDisplayOrder; + const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], displayOrder, coreDisplayOrder?.defaultOrder || []); - if (notebookFromNotebookContentProvider !== undefined) { - return notebookFromNotebookContentProvider; - } + let orderMimeTypes: IOrderedMimeType[] = []; - let provider = this._notebookProviders.get(viewType); + sorted.forEach(mimeType => { + let handlers = this.findBestMatchedRenderer(mimeType); - if (provider) { - if (!this._documents.has(URI.revive(uri).toString())) { - let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, URI.revive(uri), this); - await this._proxy.$createNotebookDocument( - document.handle, - viewType, - uri - ); + if (handlers.length) { + let renderedOutput = handlers[0].render(document, output, mimeType); - this._documents.set(URI.revive(uri).toString(), document); + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: true, + rendererId: handlers[0].handle, + output: renderedOutput + }); + + for (let i = 1; i < handlers.length; i++) { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false, + rendererId: handlers[i].handle + }); + } + + if (mimeTypeSupportedByCore(mimeType)) { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false, + rendererId: -1 + }); + } + } else { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false + }); } + }); - const onDidReceiveMessage = new Emitter(); - - let editor = new ExtHostNotebookEditor( - viewType, - `${ExtHostNotebookController._handlePool++}`, - URI.revive(uri), - this._proxy, - onDidReceiveMessage, - this._documents.get(URI.revive(uri).toString())!, - this._documentsAndEditors - ); - - this._editors.set(URI.revive(uri).toString(), { editor, onDidReceiveMessage }); - await provider.provider.resolveNotebook(editor); - // await editor.document.$updateCells(); - return editor.document.handle; - } - - return Promise.resolve(undefined); + return { + outputKind: output.outputKind, + data: output.data, + orderedMimeTypes: orderMimeTypes, + pickedMimeTypeIndex: 0 + }; } async $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise { @@ -847,15 +828,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return this._notebookContentProviders.get(viewType)!.provider.executeCell(document, cell, token); } - - let provider = this._notebookProviders.get(viewType); - - if (!provider) { - return; - } - - let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; - return provider.provider.executeCell(document!, cell, token); } async $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise { @@ -874,44 +846,26 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return true; } - let provider = this._notebookProviders.get(viewType); - - if (provider && document) { - return await provider.provider.save(document); - } - return false; } - async $updateActiveEditor(viewType: string, uri: UriComponents): Promise { - this._activeNotebookDocument = this._documents.get(URI.revive(uri).toString()); - this._activeNotebookEditor = this._editors.get(URI.revive(uri).toString())?.editor; - } - - async $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise { - let provider = this._notebookProviders.get(viewType); - - if (!provider) { + async $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise { + let document = this._documents.get(URI.revive(uri).toString()); + if (!document) { return false; } - let document = this._documents.get(URI.revive(uri).toString()); + if (this._notebookContentProviders.has(viewType)) { + try { + await this._notebookContentProviders.get(viewType)!.provider.saveNotebookAs(URI.revive(target), document, token); + } catch (e) { + return false; + } - if (document) { - document.dispose(); - this._documents.delete(URI.revive(uri).toString()); - this._onDidCloseNotebookDocument.fire(document); + return true; } - let editor = this._editors.get(URI.revive(uri).toString()); - - if (editor) { - editor.editor.dispose(); - editor.onDidReceiveMessage.dispose(); - this._editors.delete(URI.revive(uri).toString()); - } - - return true; + return false; } $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void { @@ -957,4 +911,60 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } } + + async $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta) { + if (delta.removedDocuments) { + delta.removedDocuments.forEach((uri) => { + let document = this._documents.get(URI.revive(uri).toString()); + + if (document) { + document.dispose(); + this._documents.delete(URI.revive(uri).toString()); + this._onDidCloseNotebookDocument.fire(document); + } + + let editor = this._editors.get(URI.revive(uri).toString()); + + if (editor) { + editor.editor.dispose(); + editor.onDidReceiveMessage.dispose(); + this._editors.delete(URI.revive(uri).toString()); + } + }); + } + + if (delta.addedDocuments) { + delta.addedDocuments.forEach(modelData => { + const revivedUri = URI.revive(modelData.uri); + const viewType = modelData.viewType; + if (!this._documents.has(revivedUri.toString())) { + let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, revivedUri, this); + this._documents.set(revivedUri.toString(), document); + } + + const onDidReceiveMessage = new Emitter(); + const document = this._documents.get(revivedUri.toString())!; + + let editor = new ExtHostNotebookEditor( + viewType, + `${ExtHostNotebookController._handlePool++}`, + revivedUri, + this._proxy, + onDidReceiveMessage, + document, + this._documentsAndEditors + ); + + this._onDidOpenNotebookDocument.fire(document); + + // TODO, does it already exist? + this._editors.set(revivedUri.toString(), { editor, onDidReceiveMessage }); + }); + } + + if (delta.newActiveEditor) { + this._activeNotebookDocument = this._documents.get(URI.revive(delta.newActiveEditor).toString()); + this._activeNotebookEditor = this._editors.get(URI.revive(delta.newActiveEditor).toString())?.editor; + } + } } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index bb0d6570508..ac32addcfee 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -44,6 +44,7 @@ import { LineNumbersType } from 'vs/editor/common/config/editorOptions'; import { ActivitybarPart } from 'vs/workbench/browser/parts/activitybar/activitybarPart'; import { URI } from 'vs/base/common/uri'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; export enum Settings { ACTIVITYBAR_VISIBLE = 'workbench.activityBar.visible', @@ -178,6 +179,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private notificationService!: INotificationService; private themeService!: IThemeService; private activityBarService!: IActivityBarService; + private statusBarService!: IStatusbarService; protected readonly state = { fullscreen: false, @@ -262,7 +264,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.titleService = accessor.get(ITitleService); this.notificationService = accessor.get(INotificationService); this.activityBarService = accessor.get(IActivityBarService); - accessor.get(IStatusbarService); // not used, but called to ensure instantiated + this.statusBarService = accessor.get(IStatusbarService); // Listeners this.registerLayoutListeners(); @@ -397,6 +399,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const newMenubarVisibility = getMenuBarVisibility(this.configurationService, this.environmentService); this.setMenubarVisibility(newMenubarVisibility, !!skipLayout); + // Centered Layout + this.centerEditorLayout(this.state.editor.centered, skipLayout); } private setSideBarPosition(position: Position): void { @@ -850,8 +854,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi case Parts.ACTIVITYBAR_PART: this.activityBarService.focusActivityBar(); break; + case Parts.STATUSBAR_PART: + this.statusBarService.focus(); default: - // Status Bar, Activity Bar and Title Bar simply pass focus to container + // Title Bar simply pass focus to container const container = this.getContainer(part); if (container) { container.focus(); @@ -1205,9 +1211,18 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi let smartActive = active; const activeEditor = this.editorService.activeEditor; - if (this.configurationService.getValue('workbench.editor.centeredLayoutAutoResize') - && (this.editorGroupService.groups.length > 1 || (activeEditor && activeEditor instanceof SideBySideEditorInput))) { - smartActive = false; // Respect the auto resize setting - do not go into centered layout if there is more than 1 group. + + const isSideBySideLayout = activeEditor + && activeEditor instanceof SideBySideEditorInput + // DiffEditorInput inherits from SideBySideEditorInput but can still be functionally an inline editor. + && (!(activeEditor instanceof DiffEditorInput) || this.configurationService.getValue('diffEditor.renderSideBySide')); + + const isCenteredLayoutAutoResizing = this.configurationService.getValue('workbench.editor.centeredLayoutAutoResize'); + if ( + isCenteredLayoutAutoResizing + && (this.editorGroupService.groups.length > 1 || isSideBySideLayout) + ) { + smartActive = false; } // Enter Centered Editor Layout diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index d63b7af49f4..51850da3706 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -5,23 +5,23 @@ /* Font Families (with CJK support) */ -.mac { font-family: -apple-system, BlinkMacSystemFont, sans-serif; } -.mac:lang(zh-Hans) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; } -.mac:lang(zh-Hant) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; } -.mac:lang(ja) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; } -.mac:lang(ko) { font-family: -apple-system, BlinkMacSystemFont, "Nanum Gothic", "Apple SD Gothic Neo", "AppleGothic", sans-serif; } +.mac { font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; } +.mac:lang(zh-Hans) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; } +.mac:lang(zh-Hant) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; } +.mac:lang(ja) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; } +.mac:lang(ko) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Nanum Gothic", "Apple SD Gothic Neo", "AppleGothic", sans-serif; } -.windows { font-family: "Segoe WPC", "Segoe UI", sans-serif; } -.windows:lang(zh-Hans) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; } -.windows:lang(zh-Hant) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; } -.windows:lang(ja) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; } -.windows:lang(ko) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; } +.windows { font-family: system-ui, "Segoe WPC", "Segoe UI", sans-serif; } +.windows:lang(zh-Hans) { font-family: system-ui, "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; } +.windows:lang(zh-Hant) { font-family: system-ui, "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; } +.windows:lang(ja) { font-family: system-ui, "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; } +.windows:lang(ko) { font-family: system-ui, "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; } -.linux { font-family: "Ubuntu", "Droid Sans", sans-serif; } -.linux:lang(zh-Hans) { font-family: "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } -.linux:lang(zh-Hant) { font-family: "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; } -.linux:lang(ja) { font-family: "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; } -.linux:lang(ko) { font-family: "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } +.linux { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; } +.linux:lang(zh-Hans) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } +.linux:lang(zh-Hant) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; } +.linux:lang(ja) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; } +.linux:lang(ko) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } .mac { --monaco-monospace-font: "SF Mono", Monaco, Menlo, Courier, monospace; } .windows { --monaco-monospace-font: Consolas, "Courier New", monospace; } diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index be181439013..5c01ee205f7 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -10,7 +10,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { LRUCache } from 'vs/base/common/map'; +import { LRUCache, Touch } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { isEmptyObject } from 'vs/base/common/types'; @@ -256,7 +256,9 @@ export class EditorMemento implements IEditorMemento { moveEditorState(source: URI, target: URI): void { const cache = this.doLoad(); - const cacheKeys = cache.keys(); + // We need a copy of the keys to not iterate over + // newly inserted elements. + const cacheKeys = [...cache.keys()]; for (const cacheKey of cacheKeys) { const resource = URI.parse(cacheKey); @@ -273,7 +275,8 @@ export class EditorMemento implements IEditorMemento { targetResource = joinPath(target, resource.path.substr(index + source.path.length + 1)); // parent folder got moved } - const value = cache.get(cacheKey); + // Don't modify LRU state. + const value = cache.get(cacheKey, Touch.None); if (value) { cache.delete(cacheKey); cache.set(targetResource.toString(), value); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 3da11230ff9..827a98b8a9e 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editorgroupview'; - import { EditorGroup, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup'; -import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditorPane, EditorGroupEditorsCountContext, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditorPane, EditorGroupEditorsCountContext, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, EditorStickyContext, EditorPinnedContext } from 'vs/workbench/common/editor'; import { Event, Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { addClass, addClasses, Dimension, trackFocus, toggleClass, removeClass, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor } from 'vs/base/browser/dom'; @@ -220,8 +219,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private handleGroupContextKeys(contextKeyService: IContextKeyService): void { const groupActiveEditorDirtyContextKey = EditorGroupActiveEditorDirtyContext.bindTo(contextKeyService); const groupEditorsCountContext = EditorGroupEditorsCountContext.bindTo(contextKeyService); + const groupActiveEditorPinnedContext = EditorPinnedContext.bindTo(contextKeyService); + const groupActiveEditorStickyContext = EditorStickyContext.bindTo(contextKeyService); - let activeEditorListener = new MutableDisposable(); + const activeEditorListener = new MutableDisposable(); const observeActiveEditor = () => { activeEditorListener.clear(); @@ -237,11 +238,22 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Update group contexts based on group changes this._register(this.onDidGroupChange(e => { - - // Track the active editor and update context key that reflects - // the dirty state of this editor - if (e.kind === GroupChangeKind.EDITOR_ACTIVE) { - observeActiveEditor(); + switch (e.kind) { + case GroupChangeKind.EDITOR_ACTIVE: + // Track the active editor and update context key that reflects + // the dirty state of this editor + observeActiveEditor(); + break; + case GroupChangeKind.EDITOR_PIN: + if (e.editor && e.editor === this._group.activeEditor) { + groupActiveEditorPinnedContext.set(this._group.isPinned(this._group.activeEditor)); + } + break; + case GroupChangeKind.EDITOR_STICKY: + if (e.editor && e.editor === this._group.activeEditor) { + groupActiveEditorStickyContext.set(this._group.isSticky(this._group.activeEditor)); + } + break; } // Group editors count context @@ -464,6 +476,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Model Events this._register(this._group.onDidChangeEditorPinned(editor => this.onDidChangeEditorPinned(editor))); + this._register(this._group.onDidChangeEditorSticky(editor => this.onDidChangeEditorSticky(editor))); this._register(this._group.onDidOpenEditor(editor => this.onDidOpenEditor(editor))); this._register(this._group.onDidCloseEditor(editor => this.handleOnDidCloseEditor(editor))); this._register(this._group.onDidDisposeEditor(editor => this.onDidDisposeEditor(editor))); @@ -478,11 +491,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } private onDidChangeEditorPinned(editor: EditorInput): void { - - // Event this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_PIN, editor }); } + private onDidChangeEditorSticky(editor: EditorInput): void { + this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_STICKY, editor }); + } + private onDidOpenEditor(editor: EditorInput): void { /* __GDPR__ diff --git a/src/vs/workbench/browser/parts/editor/editorsObserver.ts b/src/vs/workbench/browser/parts/editor/editorsObserver.ts index e67d77a552c..4573dbdeb1a 100644 --- a/src/vs/workbench/browser/parts/editor/editorsObserver.ts +++ b/src/vs/workbench/browser/parts/editor/editorsObserver.ts @@ -48,7 +48,7 @@ export class EditorsObserver extends Disposable { } get editors(): IEditorIdentifier[] { - return this.mostRecentEditorsMap.values(); + return [...this.mostRecentEditorsMap.values()]; } hasEditor(resource: URI): boolean { @@ -283,7 +283,7 @@ export class EditorsObserver extends Disposable { // Across all editor groups else { - await this.doEnsureOpenedEditorsLimit(limit, this.mostRecentEditorsMap.values(), exclude); + await this.doEnsureOpenedEditorsLimit(limit, [...this.mostRecentEditorsMap.values()], exclude); } } @@ -346,7 +346,7 @@ export class EditorsObserver extends Disposable { private serialize(): ISerializedEditorsList { const registry = Registry.as(Extensions.EditorInputFactories); - const entries = this.mostRecentEditorsMap.values(); + const entries = [...this.mostRecentEditorsMap.values()]; const mapGroupToSerializableEditorsOfGroup = new Map(); return { diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index ce81de97af0..992011d8288 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -290,13 +290,12 @@ padding-right: 5px; /* we need less room when sizing is shrink (unless tab is sticky) */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty-border-top > .tab-close, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty-border-top > .tab-close { display: none; /* hide dirty state when highlightModifiedTabs is enabled and when running without close button */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top) { - padding-right: 0; /* remove extra padding when we are running without close button */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top):not(.sticky) { + padding-right: 0; /* remove extra padding when we are running without close button (unless tab is sticky) */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off > .tab-close { diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index e03c9f0ea32..2be7d5f6734 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -50,11 +50,17 @@ interface ICachedPanel { views?: { when?: string }[]; } +interface IPlaceholderViewContainer { + id: string; + name?: string; +} + export class PanelPart extends CompositePart implements IPanelService { static readonly activePanelSettingsKey = 'workbench.panelpart.activepanelid'; static readonly PINNED_PANELS = 'workbench.panel.pinnedPanels'; + static readonly PLACEHOLDER_VIEW_CONTAINERS = 'workbench.panel.placeholderPanels'; private static readonly MIN_COMPOSITE_BAR_WIDTH = 50; _serviceBrand: undefined; @@ -94,6 +100,8 @@ export class PanelPart extends CompositePart implements IPanelService { private blockOpeningPanel = false; private contentDimension: Dimension | undefined; + private extensionsRegistered = false; + private panelRegistry: PanelRegistry; private dndHandler: ICompositeDragAndDrop; @@ -255,9 +263,11 @@ export class PanelPart extends CompositePart implements IPanelService { } private updateActivity(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { + const cachedTitle = this.getPlaceholderViewContainers().filter(panel => panel.id === viewContainer.id)[0]?.name; + const activity: IActivity = { id: viewContainer.id, - name: viewContainerModel.title, + name: this.extensionsRegistered || cachedTitle === undefined ? viewContainerModel.title : cachedTitle, keybindingId: viewContainer.focusCommand?.id }; @@ -268,7 +278,10 @@ export class PanelPart extends CompositePart implements IPanelService { pinnedAction.setActivity(activity); } - this.saveCachedPanels(); + // only update our cached panel info after extensions are done registering + if (this.extensionsRegistered) { + this.saveCachedPanels(); + } } private onDidChangeActiveViews(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { @@ -313,6 +326,7 @@ export class PanelPart extends CompositePart implements IPanelService { } private onDidRegisterExtensions(): void { + this.extensionsRegistered = true; this.removeNotExistingComposites(); this.saveCachedPanels(); @@ -670,6 +684,7 @@ export class PanelPart extends CompositePart implements IPanelService { private saveCachedPanels(): void { const state: ICachedPanel[] = []; + const placeholders: IPlaceholderViewContainer[] = []; const compositeItems = this.compositeBar.getCompositeBarItems(); for (const compositeItem of compositeItems) { @@ -677,10 +692,12 @@ export class PanelPart extends CompositePart implements IPanelService { if (viewContainer) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); state.push({ id: compositeItem.id, name: viewContainerModel.title, pinned: compositeItem.pinned, order: compositeItem.order, visible: compositeItem.visible }); + placeholders.push({ id: compositeItem.id, name: this.getCompositeActions(compositeItem.id).activityAction.label }); } } this.cachedPanelsValue = JSON.stringify(state); + this.setPlaceholderViewContainers(placeholders); } private getCachedPanels(): ICachedPanel[] { @@ -694,6 +711,13 @@ export class PanelPart extends CompositePart implements IPanelService { return serialized; }); + for (const placeholderViewContainer of this.getPlaceholderViewContainers()) { + const cachedViewContainer = cachedPanels.filter(cached => cached.id === placeholderViewContainer.id)[0]; + if (cachedViewContainer) { + cachedViewContainer.name = placeholderViewContainer.name; + } + } + return cachedPanels; } @@ -721,6 +745,38 @@ export class PanelPart extends CompositePart implements IPanelService { this.storageService.store(PanelPart.PINNED_PANELS, value, StorageScope.GLOBAL); } + private getPlaceholderViewContainers(): IPlaceholderViewContainer[] { + return JSON.parse(this.placeholderViewContainersValue); + } + + private setPlaceholderViewContainers(placeholderViewContainers: IPlaceholderViewContainer[]): void { + this.placeholderViewContainersValue = JSON.stringify(placeholderViewContainers); + } + + private _placeholderViewContainersValue: string | undefined; + private get placeholderViewContainersValue(): string { + if (!this._placeholderViewContainersValue) { + this._placeholderViewContainersValue = this.getStoredPlaceholderViewContainersValue(); + } + + return this._placeholderViewContainersValue; + } + + private set placeholderViewContainersValue(placeholderViewContainesValue: string) { + if (this.placeholderViewContainersValue !== placeholderViewContainesValue) { + this._placeholderViewContainersValue = placeholderViewContainesValue; + this.setStoredPlaceholderViewContainersValue(placeholderViewContainesValue); + } + } + + private getStoredPlaceholderViewContainersValue(): string { + return this.storageService.get(PanelPart.PLACEHOLDER_VIEW_CONTAINERS, StorageScope.WORKSPACE, '[]'); + } + + private setStoredPlaceholderViewContainersValue(value: string): void { + this.storageService.store(PanelPart.PLACEHOLDER_VIEW_CONTAINERS, value, StorageScope.WORKSPACE); + } + private getViewContainer(panelId: string): ViewContainer | undefined { return this.viewDescriptorService.getViewContainerById(panelId) || undefined; } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 2b74fcdbd38..fa4de06a425 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -69,6 +69,10 @@ class StatusbarViewModel extends Disposable { get entries(): IStatusbarViewModelEntry[] { return this._entries; } private hidden!: Set; + get lastFocusedEntry(): IStatusbarViewModelEntry | undefined { + return this._lastFocusedEntry && !this.isHidden(this._lastFocusedEntry.id) ? this._lastFocusedEntry : undefined; + } + private _lastFocusedEntry: IStatusbarViewModelEntry | undefined; private readonly _onDidChangeEntryVisibility = this._register(new Emitter<{ id: string, visible: boolean }>()); readonly onDidChangeEntryVisibility = this._onDidChangeEntryVisibility.event; @@ -219,6 +223,7 @@ class StatusbarViewModel extends Disposable { if (focused) { const entry = getVisibleEntry(this._entries.indexOf(focused) + delta); if (entry) { + this._lastFocusedEntry = entry; entry.labelContainer.focus(); return; } @@ -226,6 +231,7 @@ class StatusbarViewModel extends Disposable { const entry = getVisibleEntry(restartPosition); if (entry) { + this._lastFocusedEntry = entry; entry.labelContainer.focus(); } } @@ -493,6 +499,15 @@ export class StatusbarPart extends Part implements IStatusbarService { this.viewModel.focusPreviousEntry(); } + focus(preserveEntryFocus = true): void { + this.getContainer()?.focus(); + const lastFocusedEntry = this.viewModel.lastFocusedEntry; + if (preserveEntryFocus && lastFocusedEntry) { + // Need a timeout, for some reason without it the inner label container will not get focused + setTimeout(() => lastFocusedEntry.labelContainer.focus(), 0); + } + } + createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; @@ -680,10 +695,6 @@ export class StatusbarPart extends Part implements IStatusbarService { return itemContainer; } - focus(): void { - this.getContainer(); - } - layout(width: number, height: number): void { super.layout(width, height); super.layoutContents(width, height); @@ -935,3 +946,14 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ statusBarService.focusNextEntry(); } }); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.statusBar.clearFocus', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape, + when: CONTEXT_STATUS_BAR_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const statusBarService = accessor.get(IStatusbarService); + statusBarService.focus(false); + } +}); diff --git a/src/vs/workbench/browser/parts/views/media/paneviewlet.css b/src/vs/workbench/browser/parts/views/media/paneviewlet.css index 318e590df46..3679406c0fb 100644 --- a/src/vs/workbench/browser/parts/views/media/paneviewlet.css +++ b/src/vs/workbench/browser/parts/views/media/paneviewlet.css @@ -18,6 +18,15 @@ .monaco-pane-view .pane > .pane-header > .actions.show { display: initial; } +.monaco-pane-view .pane > .pane-header .icon { + display: none; + width: 16px; + height: 16px; +} + +.monaco-pane-view .pane.pane.horizontal:not(.expanded) > .pane-header .icon { + display: inline; +} .monaco-pane-view .pane > .pane-header h3.title { white-space: nowrap; @@ -28,6 +37,11 @@ -webkit-margin-after: 0; } +.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header > h3.title, +.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header > .description { + display: none; +} + .monaco-pane-view .pane .monaco-progress-container { position: absolute; left: 0; diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 2b2881698f2..31f19314b5b 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -6,10 +6,10 @@ import 'vs/css!./media/paneviewlet'; import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { ColorIdentifier, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { ColorIdentifier, activeContrastBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, EDITOR_DRAG_AND_DROP_BACKGROUND, PANEL_BORDER } from 'vs/workbench/common/theme'; -import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass } from 'vs/base/browser/dom'; +import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass, createCSSRule, asCSSUrl, addClasses } from 'vs/base/browser/dom'; import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { firstIndex } from 'vs/base/common/arrays'; import { IAction } from 'vs/base/common/actions'; @@ -20,14 +20,14 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; -import { PaneView, IPaneViewOptions, IPaneOptions, Pane } from 'vs/base/browser/ui/splitview/paneview'; +import { PaneView, IPaneViewOptions, IPaneOptions, Pane, IPaneStyles } from 'vs/base/browser/ui/splitview/paneview'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IViewsRegistry, IViewContentDescriptor, IAddedViewDescriptorRef, IViewDescriptorRef, IViewContainerModel } from 'vs/workbench/common/views'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { assertIsDefined } from 'vs/base/common/types'; +import { assertIsDefined, isString } from 'vs/base/common/types'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -48,6 +48,7 @@ import { IProgressIndicator } from 'vs/platform/progress/common/progress'; import { RunOnceScheduler } from 'vs/base/common/async'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { URI } from 'vs/base/common/uri'; export interface IPaneColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -185,6 +186,7 @@ export abstract class ViewPane extends Pane implements IView { private readonly showActionsAlways: boolean = false; private headerContainer?: HTMLElement; private titleContainer?: HTMLElement; + private iconContainer?: HTMLElement; protected twistiesContainer?: HTMLElement; private bodyContainer!: HTMLElement; @@ -290,6 +292,10 @@ export abstract class ViewPane extends Pane implements IView { this._register(this.toolbar); this.setActions(); + this._register(this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!.onDidChangeContainerInfo(({ title }) => { + this.updateTitle(this.title); + })); + const onDidRelevantConfigurationChange = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ViewPane.AlwaysShowActionsConfig)); this._register(onDidRelevantConfigurationChange(this.updateActionsVisibility, this)); this.updateActionsVisibility(); @@ -299,18 +305,83 @@ export abstract class ViewPane extends Pane implements IView { this.twistiesContainer = append(container, $('.twisties.codicon.codicon-chevron-right')); } - protected renderHeaderTitle(container: HTMLElement, title: string): void { - this.titleContainer = append(container, $('h3.title', undefined, title)); + style(styles: IPaneStyles): void { + super.style(styles); + + const icon = this.viewDescriptorService.getViewDescriptorById(this.id)?.containerIcon; + + if (this.iconContainer) { + const fgColor = styles.headerForeground || this.themeService.getColorTheme().getColor(foreground); + if (URI.isUri(icon)) { + // Apply background color to activity bar item provided with iconUrls + this.iconContainer.style.backgroundColor = fgColor ? fgColor.toString() : ''; + this.iconContainer.style.color = ''; + } else { + // Apply foreground color to activity bar items provided with codicons + this.iconContainer.style.color = fgColor ? fgColor.toString() : ''; + this.iconContainer.style.backgroundColor = ''; + } + } } - protected updateTitle(title: string): void { - if (this.titleContainer) { - this.titleContainer.textContent = title; + protected renderHeaderTitle(container: HTMLElement, title: string): void { + this.iconContainer = append(container, $('.icon', undefined)); + const icon = this.viewDescriptorService.getViewDescriptorById(this.id)?.containerIcon; + + let cssClass: string | undefined = undefined; + if (URI.isUri(icon)) { + cssClass = `view-${this.id.replace(/[\.\:]/g, '-')}`; + const iconClass = `.pane-header .icon.${cssClass}`; + + createCSSRule(iconClass, ` + mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + mask-size: 24px; + -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + -webkit-mask-size: 16px; + `); + } else if (isString(icon)) { + addClass(this.iconContainer, 'codicon'); + cssClass = icon; } + + if (cssClass) { + addClasses(this.iconContainer, cssClass); + } + + const calculatedTitle = this.calculateTitle(title); + this.titleContainer = append(container, $('h3.title', undefined, calculatedTitle)); + this.iconContainer.title = calculatedTitle; + this.iconContainer.setAttribute('aria-label', calculatedTitle); + } + + updateTitle(title: string): void { + const calculatedTitle = this.calculateTitle(title); + if (this.titleContainer) { + this.titleContainer.textContent = calculatedTitle; + } + + if (this.iconContainer) { + this.iconContainer.title = calculatedTitle; + this.iconContainer.setAttribute('aria-label', calculatedTitle); + } + this.title = title; this._onDidChangeTitleArea.fire(); } + private calculateTitle(title: string): string { + const viewContainer = this.viewDescriptorService.getViewContainerByViewId(this.id)!; + const model = this.viewDescriptorService.getViewContainerModel(viewContainer); + const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.id)!; + const isDefault = this.viewDescriptorService.getDefaultContainerById(this.id) === viewContainer; + + if (!isDefault && viewDescriptor.containerTitle && model.title !== viewDescriptor.containerTitle) { + return `${viewDescriptor.containerTitle}: ${title}`; + } + + return title; + } + private scrollableElement!: DomScrollableElement; protected renderBody(container: HTMLElement): void { @@ -510,7 +581,6 @@ export abstract class ViewPane extends Pane implements IView { export interface IViewPaneContainerOptions extends IPaneViewOptions { mergeViewWithContainerWhenSingleView: boolean; - donotShowContainerTitleWhenMergedWithContainer?: boolean; } interface IViewPaneItem { @@ -904,7 +974,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { if (this.isViewMergedWithContainer()) { const paneItemTitle = this.paneItems[0].pane.title; - if (this.options.donotShowContainerTitleWhenMergedWithContainer || containerTitle === paneItemTitle) { + if (containerTitle === paneItemTitle) { return this.paneItems[0].pane.title; } return paneItemTitle ? `${containerTitle}: ${paneItemTitle}` : containerTitle; @@ -1227,6 +1297,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this.updateTitleArea(); } }); + const onDidChangeVisibility = pane.onDidChangeBodyVisibility(() => this._onDidChangeViewVisibility.fire(pane)); const onDidChange = pane.onDidChange(() => { if (pane === this.lastFocusedPane && !pane.isExpanded()) { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 7c8c575f891..18196e719a3 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -34,7 +34,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio }, 'workbench.editor.scrollToSwitchTabs': { 'type': 'boolean', - 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls wether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration."), + 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration."), 'default': false }, 'workbench.editor.highlightModifiedTabs': { diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 13661dd8d18..178aa1d792f 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -83,6 +83,9 @@ export class EditorGroup extends Disposable { private readonly _onDidChangeEditorPinned = this._register(new Emitter()); readonly onDidChangeEditorPinned = this._onDidChangeEditorPinned.event; + private readonly _onDidChangeEditorSticky = this._register(new Emitter()); + readonly onDidChangeEditorSticky = this._onDidChangeEditorSticky.event; + //#endregion private _id: GroupIdentifier; @@ -122,6 +125,12 @@ export class EditorGroup extends Disposable { private onConfigurationUpdated(): void { this.editorOpenPositioning = this.configurationService.getValue('workbench.editor.openPositioning'); this.focusRecentEditorAfterClose = this.configurationService.getValue('workbench.editor.focusRecentEditorAfterClose'); + + if (this.configurationService.getValue('workbench.editor.showTabs') === false) { + // Disabling tabs disables sticky editors until we support + // an indication of sticky editors when tabs are disabled + this.sticky = -1; + } } get count(): number { @@ -554,6 +563,9 @@ export class EditorGroup extends Disposable { // Adjust sticky index this.sticky++; + + // Event + this._onDidChangeEditorSticky.fire(editor); } unstick(candidate: EditorInput): EditorInput | undefined { @@ -579,6 +591,9 @@ export class EditorGroup extends Disposable { // Adjust sticky index this.sticky--; + + // Event + this._onDidChangeEditorSticky.fire(editor); } isSticky(candidateOrIndex: EditorInput | number): boolean { diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index d5336b2df02..2f7bca6e769 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -510,31 +510,31 @@ export const TITLE_BAR_ACTIVE_FOREGROUND = registerColor('titleBar.activeForegro dark: '#CCCCCC', light: '#333333', hc: '#FFFFFF' -}, nls.localize('titleBarActiveForeground', "Title bar foreground when the window is active. Note that this color is currently only supported on macOS.")); +}, nls.localize('titleBarActiveForeground', "Title bar foreground when the window is active.")); export const TITLE_BAR_INACTIVE_FOREGROUND = registerColor('titleBar.inactiveForeground', { dark: transparent(TITLE_BAR_ACTIVE_FOREGROUND, 0.6), light: transparent(TITLE_BAR_ACTIVE_FOREGROUND, 0.6), hc: null -}, nls.localize('titleBarInactiveForeground', "Title bar foreground when the window is inactive. Note that this color is currently only supported on macOS.")); +}, nls.localize('titleBarInactiveForeground', "Title bar foreground when the window is inactive.")); export const TITLE_BAR_ACTIVE_BACKGROUND = registerColor('titleBar.activeBackground', { dark: '#3C3C3C', light: '#DDDDDD', hc: '#000000' -}, nls.localize('titleBarActiveBackground', "Title bar background when the window is active. Note that this color is currently only supported on macOS.")); +}, nls.localize('titleBarActiveBackground', "Title bar background when the window is active.")); export const TITLE_BAR_INACTIVE_BACKGROUND = registerColor('titleBar.inactiveBackground', { dark: transparent(TITLE_BAR_ACTIVE_BACKGROUND, 0.6), light: transparent(TITLE_BAR_ACTIVE_BACKGROUND, 0.6), hc: null -}, nls.localize('titleBarInactiveBackground', "Title bar background when the window is inactive. Note that this color is currently only supported on macOS.")); +}, nls.localize('titleBarInactiveBackground', "Title bar background when the window is inactive.")); export const TITLE_BAR_BORDER = registerColor('titleBar.border', { dark: null, light: null, hc: contrastBorder -}, nls.localize('titleBarBorder', "Title bar border color. Note that this color is currently only supported on macOS.")); +}, nls.localize('titleBarBorder', "Title bar border color.")); // < --- Menubar --- > diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index c8bffe666da..6b7a1506c99 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -211,6 +211,8 @@ export interface IViewDescriptor { readonly containerIcon?: string | URI; + readonly containerTitle?: string; + // Applies only to newly created views readonly hideByDefault?: boolean; diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 045c2470162..c1201838060 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -311,7 +311,7 @@ function getSuggestEnabledInputOptions(ariaLabel?: string): IEditorOptions { roundedSelection: false, renderIndentGuides: false, cursorWidth: 1, - fontFamily: ' -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif', + fontFamily: ' system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif', ariaLabel: ariaLabel || '', snippetSuggestions: 'none', diff --git a/src/vs/workbench/contrib/configExporter/electron-browser/configurationExportHelper.ts b/src/vs/workbench/contrib/configExporter/electron-browser/configurationExportHelper.ts index 083e5881f22..1fcc461ca7d 100644 --- a/src/vs/workbench/contrib/configExporter/electron-browser/configurationExportHelper.ts +++ b/src/vs/workbench/contrib/configExporter/electron-browser/configurationExportHelper.ts @@ -61,7 +61,8 @@ export class DefaultConfigurationExportHelper { const processProperty = (name: string, prop: IConfigurationPropertySchema) => { if (processedNames.has(name)) { - throw new Error('Setting is registered twice: ' + name); + console.warn('Setting is registered twice: ' + name); + return; } processedNames.add(name); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 97b9c5bfaaf..95fecef9b18 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -431,6 +431,10 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo const currentEditor = group?.editors.find(editor => isEqual(editor.resource, resource)); const customEditors = this.customEditorService.getAllCustomEditors(resource); + if (!customEditors.length) { + return []; + } + return [ { ...defaultEditorOverrideEntry, diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 4e8fb70e974..8a691c2ccb1 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -61,6 +61,7 @@ export class BreakpointsView extends ViewPane { private list!: WorkbenchList; private needsRefresh = false; + private ignoreLayout = false; constructor( options: IViewletViewOptions, @@ -79,7 +80,6 @@ export class BreakpointsView extends ViewPane { ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.updateSize(); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); } @@ -164,10 +164,20 @@ export class BreakpointsView extends ViewPane { } protected layoutBody(height: number, width: number): void { + if (this.ignoreLayout) { + return; + } + super.layoutBody(height, width); if (this.list) { this.list.layout(height, width); } + try { + this.ignoreLayout = true; + this.updateSize(); + } finally { + this.ignoreLayout = false; + } } private onListContextMenu(e: IListContextMenuEvent): void { diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 4a1a948276a..9f65698bf2d 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -475,7 +475,7 @@ class SessionsRenderer implements ITreeRenderer { diff --git a/src/vs/workbench/contrib/debug/browser/debugActions.ts b/src/vs/workbench/contrib/debug/browser/debugActions.ts index 9c62710d39e..a0ad5e6d2b8 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActions.ts @@ -81,7 +81,7 @@ export class ConfigureAction extends AbstractDebugAction { this.class = configurationManager.selectedConfiguration.name ? 'debug-action codicon codicon-gear' : 'debug-action codicon codicon-gear notification'; } - async run(event?: any): Promise { + async run(): Promise { if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { this.notificationService.info(nls.localize('noFolderDebugConfig', "Please first open a folder in order to do advanced debug configuration.")); return; @@ -105,8 +105,7 @@ export class ConfigureAction extends AbstractDebugAction { } if (launch) { - const sideBySide = !!(event && (event.ctrlKey || event.metaKey)); - return launch.openConfigFile(sideBySide, false); + return launch.openConfigFile(false); } } } diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 944836a1b5e..cc3f614f34c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -504,7 +504,7 @@ export function registerCommands(): void { const launch = manager.getLaunches().find(l => l.uri.toString() === launchUri) || manager.selectedConfiguration.launch; if (launch) { - const { editor, created } = await launch.openConfigFile(false, false); + const { editor, created } = await launch.openConfigFile(false); if (editor && !created) { const codeEditor = editor.getControl(); if (codeEditor) { diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 64e14cfea9f..14385280e0b 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IConfigPresentation } from 'vs/workbench/contrib/debug/common/debug'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; -import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; @@ -635,7 +635,7 @@ class Launch extends AbstractLaunch implements ILaunch { return this.configurationService.inspect('launch', { resource: this.workspace.uri }).workspaceFolderValue; } - async openConfigFile(sideBySide: boolean, preserveFocus: boolean, type?: string, token?: CancellationToken): Promise<{ editor: IEditorPane | null, created: boolean }> { + async openConfigFile(preserveFocus: boolean, type?: string, token?: CancellationToken): Promise<{ editor: IEditorPane | null, created: boolean }> { const resource = this.uri; let created = false; let content = ''; @@ -681,7 +681,7 @@ class Launch extends AbstractLaunch implements ILaunch { pinned: created, revealIfVisible: true }, - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + }, ACTIVE_GROUP); return ({ editor: withUndefinedAsNull(editor), @@ -715,12 +715,12 @@ class WorkspaceLaunch extends AbstractLaunch implements ILaunch { return this.configurationService.inspect('launch').workspaceValue; } - async openConfigFile(sideBySide: boolean, preserveFocus: boolean): Promise<{ editor: IEditorPane | null, created: boolean }> { + async openConfigFile(preserveFocus: boolean): Promise<{ editor: IEditorPane | null, created: boolean }> { const editor = await this.editorService.openEditor({ resource: this.contextService.getWorkspace().configuration!, options: { preserveFocus } - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + }, ACTIVE_GROUP); return ({ editor: withUndefinedAsNull(editor), @@ -758,7 +758,7 @@ class UserLaunch extends AbstractLaunch implements ILaunch { return this.configurationService.inspect('launch').userValue; } - async openConfigFile(_: boolean, preserveFocus: boolean): Promise<{ editor: IEditorPane | null, created: boolean }> { + async openConfigFile(preserveFocus: boolean): Promise<{ editor: IEditorPane | null, created: boolean }> { const editor = await this.preferencesService.openGlobalSettings(true, { preserveFocus }); return ({ editor: withUndefinedAsNull(editor), diff --git a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts index cbf747da6a7..e4c1c9427a2 100644 --- a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts +++ b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts @@ -60,7 +60,7 @@ export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider { - config.launch.openConfigFile(false, false); + config.launch.openConfigFile(false); return TriggerAction.CLOSE_PICKER; }, diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 401080dc290..203c7f85a30 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -405,7 +405,7 @@ export class DebugService implements IDebugService { const cfg = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, type, resolvedConfig, initCancellationToken.token); if (!cfg) { if (launch && type && cfg === null && !initCancellationToken.token.isCancellationRequested) { // show launch.json only for "config" being "null". - await launch.openConfigFile(false, true, type, initCancellationToken.token); + await launch.openConfigFile(true, type, initCancellationToken.token); } return false; } @@ -439,7 +439,7 @@ export class DebugService implements IDebugService { await this.showError(nls.localize('noFolderWorkspaceDebugError', "The active file can not be debugged. Make sure it is saved and that you have a debug extension installed for that file type.")); } if (launch && !initCancellationToken.token.isCancellationRequested) { - await launch.openConfigFile(false, true, undefined, initCancellationToken.token); + await launch.openConfigFile(true, undefined, initCancellationToken.token); } return false; @@ -447,7 +447,7 @@ export class DebugService implements IDebugService { } if (launch && type && configByProviders === null && !initCancellationToken.token.isCancellationRequested) { // show launch.json only for "config" being "null". - await launch.openConfigFile(false, true, type, initCancellationToken.token); + await launch.openConfigFile(true, type, initCancellationToken.token); } return false; diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 693874d0d8f..9e944c02c83 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -366,7 +366,7 @@ export class DebugSession implements IDebugSession { } } - async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean }> { + async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean } | undefined> { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'data breakpoints info')); } @@ -592,7 +592,7 @@ export class DebugSession implements IDebugSession { } const response = await this.raw.loadedSources({}); - if (response.body && response.body.sources) { + if (response && response.body && response.body.sources) { return response.body.sources.map(src => this.getSource(src)); } else { return []; @@ -735,6 +735,7 @@ export class DebugSession implements IDebugSession { await this.raw.configurationDone(); } catch (e) { // Disconnect the debug session on configuration done error #10596 + this.notificationService.error(e); if (this.raw) { this.raw.disconnect(); } diff --git a/src/vs/workbench/contrib/debug/browser/media/debugHover.css b/src/vs/workbench/contrib/debug/browser/media/debugHover.css index 694ccd0d5f3..d9a5672b41f 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugHover.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugHover.css @@ -39,6 +39,7 @@ .monaco-editor .debug-hover-widget .debug-hover-tree .monaco-list-row .monaco-tl-contents { user-select: text; -webkit-user-select: text; + white-space: pre; } /* Disable tree highlight in debug hover tree. */ @@ -56,7 +57,6 @@ } .monaco-editor .debug-hover-widget .value { - white-space: pre-wrap; color: rgba(108, 108, 108, 0.8); overflow: auto; font-family: var(--monaco-monospace-font); diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 16bdbd92f9c..c677a1fdd2a 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -87,7 +87,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private replInputContainer!: HTMLElement; private dimension!: dom.Dimension; private replInputLineCount = 1; - private model!: ITextModel; + private model: ITextModel | undefined; private historyNavigationEnablement!: IContextKey; private scopedInstantiationService!: IInstantiationService; private replElementsChangeListener: IDisposable | undefined; @@ -271,7 +271,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { if (isCodeEditor(activeEditorControl)) { this.modelChangeListener.dispose(); this.modelChangeListener = activeEditorControl.onDidChangeModelLanguage(() => this.setMode()); - if (activeEditorControl.hasModel()) { + if (this.model && activeEditorControl.hasModel()) { this.model.setMode(activeEditorControl.getModel().getLanguageIdentifier()); } } @@ -397,16 +397,18 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { getVisibleContent(): string { let text = ''; - const lineDelimiter = this.textResourcePropertiesService.getEOL(this.model.uri); - const traverseAndAppend = (node: ITreeNode) => { - node.children.forEach(child => { - text += child.element.toString().trimRight() + lineDelimiter; - if (!child.collapsed && child.children.length) { - traverseAndAppend(child); - } - }); - }; - traverseAndAppend(this.tree.getNode()); + if (this.model) { + const lineDelimiter = this.textResourcePropertiesService.getEOL(this.model.uri); + const traverseAndAppend = (node: ITreeNode) => { + node.children.forEach(child => { + text += child.element.toString().trimRight() + lineDelimiter; + if (!child.collapsed && child.children.length) { + traverseAndAppend(child); + } + }); + }; + traverseAndAppend(this.tree.getNode()); + } return removeAnsiEscapeCodes(text); } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index cbcd596bd39..d9c75bd8be4 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -194,8 +194,8 @@ export class VariablesView extends ViewPane { } if (session && session.capabilities.supportsDataBreakpoints) { const response = await session.dataBreakpointInfo(variable.name, variable.parent.reference); - const dataid = response.dataId; - if (dataid) { + const dataid = response?.dataId; + if (response && dataid) { actions.push(new Separator()); actions.push(new Action('debug.breakWhenValueChanges', nls.localize('breakWhenValueChanges', "Break When Value Changes"), undefined, true, () => { return this.debugService.addDataBreakpoint(response.description, dataid, !!response.canPersist, response.accessTypes); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 601a098796e..d270afdffbc 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -214,7 +214,7 @@ export interface IDebugSession extends ITreeElement { sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise; - dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean, accessTypes?: DebugProtocol.DataBreakpointAccessType[] }>; + dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean, accessTypes?: DebugProtocol.DataBreakpointAccessType[] } | undefined>; sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; breakpointsLocations(uri: uri, lineNumber: number): Promise; @@ -717,7 +717,7 @@ export interface ILaunch { /** * Opens the launch.json file. Creates if it does not exist. */ - openConfigFile(sideBySide: boolean, preserveFocus: boolean, type?: string, token?: CancellationToken): Promise<{ editor: IEditorPane | null, created: boolean }>; + openConfigFile(preserveFocus: boolean, type?: string, token?: CancellationToken): Promise<{ editor: IEditorPane | null, created: boolean }>; } // Debug service interfaces diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 5ce7663389a..7d95a04c6d2 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -144,7 +144,7 @@ export class MockSession implements IDebugSession { throw new Error('Method not implemented.'); } - dataBreakpointInfo(name: string, variablesReference?: number | undefined): Promise<{ dataId: string | null; description: string; canPersist?: boolean | undefined; }> { + dataBreakpointInfo(name: string, variablesReference?: number | undefined): Promise<{ dataId: string | null; description: string; canPersist?: boolean | undefined; } | undefined> { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 7000e486115..a08e08da5cd 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -233,7 +233,7 @@ export class InstallAction extends ExtensionAction { const extension = await this.install(this.extension); - alert(localize('installExtensionComplete', "Installing extension {0} is completed. Please reload Visual Studio Code to enable it.", this.extension.displayName)); + alert(localize('installExtensionComplete', "Installing extension {0} is completed.", this.extension.displayName)); if (extension && extension.local) { const runningExtension = await this.getRunningExtension(extension.local); @@ -1359,7 +1359,7 @@ export class ReloadAction extends ExtensionAction { this.enabled = true; this.label = localize('reloadRequired', "Reload Required"); this.tooltip = localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); - alert(localize('installExtensionComplete', "Installing extension {0} is completed. Please reload Visual Studio Code to enable it.", this.extension.displayName)); + alert(localize('installExtensionCompletedAndReloadRequired', "Installing extension {0} is completed. Please reload Visual Studio Code to enable it.", this.extension.displayName)); return; } } diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index f0078fb7192..b77e6fb7065 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -60,8 +60,7 @@ } .dirty-count.monaco-count-badge { - padding-top: 2px; - padding-bottom: 2px; + padding: 2px 4px; margin-left: 6px; min-height: auto; } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index a7484198393..7458682b6c6 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -29,7 +29,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'vs/base/common/path'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; -import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers'; +import { compareFileExtensionsNumeric, compareFileNamesNumeric } from 'vs/base/common/comparers'; import { fillResourceDataTransfers, CodeDataTransfers, extractResources, containsDragType } from 'vs/workbench/browser/dnd'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; @@ -654,7 +654,7 @@ export class FileSorter implements ITreeSorter { } if (statA.isDirectory && statB.isDirectory) { - return compareFileNames(statA.name, statB.name); + return compareFileNamesNumeric(statA.name, statB.name); } break; @@ -688,17 +688,17 @@ export class FileSorter implements ITreeSorter { // Sort Files switch (sortOrder) { case 'type': - return compareFileExtensions(statA.name, statB.name); + return compareFileExtensionsNumeric(statA.name, statB.name); case 'modified': if (statA.mtime !== statB.mtime) { return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1; } - return compareFileNames(statA.name, statB.name); + return compareFileNamesNumeric(statA.name, statB.name); default: /* 'default', 'mixed', 'filesFirst' */ - return compareFileNames(statA.name, statB.name); + return compareFileNamesNumeric(statA.name, statB.name); } } } diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index f6dd1068a03..dfdbc4bac2c 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -184,7 +184,7 @@ export class OpenEditorsView extends ViewPane { super.renderHeaderTitle(container, this.title); const count = dom.append(container, $('.count')); - this.dirtyCountElement = dom.append(count, $('.dirty-count.monaco-count-badge')); + this.dirtyCountElement = dom.append(count, $('.dirty-count.monaco-count-badge.long')); this._register((attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => { const background = colors.badgeBackground ? colors.badgeBackground.toString() : ''; diff --git a/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts b/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts index c4d4036dc85..ff17e5b4347 100644 --- a/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts +++ b/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts @@ -46,7 +46,7 @@ export class ConfigureLocaleAction extends Action { .concat({ label: localize('installAdditionalLanguages', "Install additional languages...") }); } - public async run(event?: any): Promise { + public async run(): Promise { const languageOptions = await this.getLanguageOptions(); const currentLanguageIndex = firstIndex(languageOptions, l => l.label === language); diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index fb156b10bb1..e65cd871273 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// Scrollable Element + +export const SCROLLABLE_ELEMENT_PADDING_TOP = 16; + // Cell sizing related export const CELL_MARGIN = 20; export const CELL_RUN_GUTTER = 32; @@ -17,3 +21,4 @@ export const EDITOR_TOP_MARGIN = 0; // Top and bottom padding inside the monaco editor in a cell, which are included in `cell.editorHeight` export const EDITOR_TOP_PADDING = 12; export const EDITOR_BOTTOM_PADDING = 12; + diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 6627521eb1d..763dbc7199d 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -4,24 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { URI } from 'vs/base/common/uri'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; import { Action2, IAction2Options, MenuId, MenuItemAction, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext, InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { BaseCellRenderTemplate, CellEditState, CellRunState, ICellViewModel, INotebookEditor, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_EDITABLE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { URI } from 'vs/base/common/uri'; +import { BaseCellRenderTemplate, CellEditState, CellRunState, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; // Notebook Commands const EXECUTE_NOTEBOOK_COMMAND_ID = 'notebook.execute'; @@ -66,6 +66,8 @@ const EXECUTE_CELL_INSERT_BELOW = 'notebook.cell.executeAndInsertBelow'; const CLEAR_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.clearOutputs'; const CHANGE_CELL_LANGUAGE = 'notebook.cell.changeLanguage'; +const FOCUS_IN_OUTPUT_COMMAND_ID = 'notebook.cell.focusInOutput'; +const FOCUS_OUT_OUTPUT_COMMAND_ID = 'notebook.cell.focusOutOutput'; export const NOTEBOOK_ACTIONS_CATEGORY = localize('notebookActions.category', "Notebook"); @@ -96,6 +98,7 @@ registerAction2(class extends Action2 { }, weight: EDITOR_WIDGET_ACTION_WEIGHT }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), icon: { id: 'codicon/play' }, f1: true }); @@ -120,6 +123,7 @@ registerAction2(class extends Action2 { title: localize('notebookActions.cancel', "Stop Cell Execution"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/primitive-square' }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } @@ -183,7 +187,9 @@ registerAction2(class extends Action2 { when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.Shift | KeyCode.Enter, weight: EDITOR_WIDGET_ACTION_WEIGHT - } + }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + f1: true }); } @@ -207,11 +213,11 @@ registerAction2(class extends Action2 { // Try to select below, fall back on inserting const nextCell = editor.viewModel?.viewCells[idx + 1]; if (nextCell) { - editor.focusNotebookCell(nextCell, activeCell.editState === CellEditState.Editing); + editor.focusNotebookCell(nextCell, activeCell.editState === CellEditState.Editing ? 'editor' : 'container'); } else { const newCell = editor.insertNotebookCell(activeCell, CellKind.Code, 'below'); if (newCell) { - editor.focusNotebookCell(newCell, true); + editor.focusNotebookCell(newCell, 'editor'); } } } @@ -227,7 +233,9 @@ registerAction2(class extends Action2 { when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.Alt | KeyCode.Enter, weight: EDITOR_WIDGET_ACTION_WEIGHT - } + }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + f1: true }); } @@ -245,7 +253,7 @@ registerAction2(class extends Action2 { const newCell = editor.insertNotebookCell(activeCell, CellKind.Code, 'below'); if (newCell) { - editor.focusNotebookCell(newCell, true); + editor.focusNotebookCell(newCell, 'editor'); } } }); @@ -256,6 +264,7 @@ registerAction2(class extends Action2 { id: EXECUTE_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.executeNotebook', "Execute Notebook"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } @@ -277,6 +286,7 @@ registerAction2(class extends Action2 { id: CANCEL_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.cancelNotebook', "Cancel Notebook Execution"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } @@ -302,7 +312,8 @@ registerAction2(class extends Action2 { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext), primary: KeyCode.Escape, weight: EDITOR_WIDGET_ACTION_WEIGHT - 5 - } + }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), }); } @@ -320,7 +331,7 @@ registerAction2(class extends Action2 { activeCell.editState = CellEditState.Preview; } - editor.focusNotebookCell(activeCell, false); + editor.focusNotebookCell(activeCell, 'container'); } } }); @@ -373,6 +384,7 @@ registerAction2(class extends Action2 { weight: KeybindingWeight.WorkbenchContrib }, category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true }); } @@ -393,6 +405,7 @@ registerAction2(class extends Action2 { weight: KeybindingWeight.WorkbenchContrib }, category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true }); } @@ -405,7 +418,7 @@ registerAction2(class extends Action2 { export function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor | undefined { // TODO can `isNotebookEditor` be on INotebookEditor to avoid a circular dependency? const activeEditorPane = editorService.activeEditorPane as any | undefined; - return activeEditorPane?.isNotebookEditor ? activeEditorPane : undefined; + return activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined; } async function runActiveCell(accessor: ServicesAccessor): Promise { @@ -473,7 +486,7 @@ export async function changeCellToKind(kind: CellKind, context: INotebookCellAct newCell.model.language = language; } - notebookEditor.focusNotebookCell(newCell, cell.editState === CellEditState.Editing); + notebookEditor.focusNotebookCell(newCell, cell.editState === CellEditState.Editing ? 'editor' : 'container'); notebookEditor.deleteNotebookCell(cell); return newCell; @@ -528,7 +541,7 @@ abstract class InsertCellCommand extends Action2 { const newCell = context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction, undefined, context.ui); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, true); + context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } } @@ -540,12 +553,13 @@ registerAction2(class extends InsertCellCommand { id: INSERT_CODE_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.insertCodeCellAbove', "Insert Code Cell Above"), category: NOTEBOOK_ACTIONS_CATEGORY, - f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + f1: true }, CellKind.Code, 'above'); @@ -577,12 +591,13 @@ registerAction2(class extends InsertCellCommand { title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/add' }, - f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyCode.Enter, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + f1: true, }, CellKind.Code, 'below'); @@ -596,6 +611,7 @@ registerAction2(class extends InsertCellCommand { id: INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true }, CellKind.Markdown, @@ -627,6 +643,7 @@ registerAction2(class extends InsertCellCommand { id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true }, CellKind.Markdown, @@ -699,7 +716,6 @@ registerAction2(class extends Action2 { } }); - registerAction2(class extends Action2 { constructor() { super( @@ -721,6 +737,7 @@ registerAction2(class extends Action2 { weight: KeybindingWeight.WorkbenchContrib }, icon: { id: 'codicon/trash' }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true }); } @@ -740,12 +757,12 @@ registerAction2(class extends Action2 { // deletion succeeds, move focus to the next cell const nextCellIdx = index < context.notebookEditor.viewModel!.length ? index : context.notebookEditor.viewModel!.length - 1; if (nextCellIdx >= 0) { - context.notebookEditor.focusNotebookCell(context.notebookEditor.viewModel!.viewCells[nextCellIdx], false); + context.notebookEditor.focusNotebookCell(context.notebookEditor.viewModel!.viewCells[nextCellIdx], 'container'); } else { // No cells left, insert a new empty one const newCell = context.notebookEditor.insertNotebookCell(undefined, context.cell.cellKind); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, true); + context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } } @@ -759,7 +776,7 @@ async function moveCell(context: INotebookCellActionContext, direction: 'up' | ' if (result) { // move cell command only works when the cell container has focus - context.notebookEditor.focusNotebookCell(context.cell, false); + context.notebookEditor.focusNotebookCell(context.cell, 'container'); } } @@ -768,7 +785,7 @@ async function copyCell(context: INotebookCellActionContext, direction: 'up' | ' const newCellDirection = direction === 'up' ? 'above' : 'below'; const newCell = context.notebookEditor.insertNotebookCell(context.cell, context.cell.cellKind, newCellDirection, text); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, false); + context.notebookEditor.focusNotebookCell(newCell, 'container'); } } @@ -780,6 +797,7 @@ registerAction2(class extends Action2 { title: localize('notebookActions.moveCellUp', "Move Cell Up"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/arrow-up' }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true, keybinding: { primary: KeyMod.Alt | KeyCode.UpArrow, @@ -809,6 +827,7 @@ registerAction2(class extends Action2 { title: localize('notebookActions.moveCellDown', "Move Cell Down"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/arrow-down' }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true, keybinding: { primary: KeyMod.Alt | KeyCode.DownArrow, @@ -837,6 +856,7 @@ registerAction2(class extends Action2 { id: COPY_CELL_COMMAND_ID, title: localize('notebookActions.copy', "Copy Cell"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -868,6 +888,7 @@ registerAction2(class extends Action2 { id: CUT_CELL_COMMAND_ID, title: localize('notebookActions.cut', "Cut Cell"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -906,6 +927,7 @@ registerAction2(class extends Action2 { id: PASTE_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.pasteAbove', "Paste Cell Above"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -947,6 +969,7 @@ registerAction2(class extends Action2 { id: PASTE_CELL_COMMAND_ID, title: localize('notebookActions.paste', "Paste Cell"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -989,6 +1012,7 @@ registerAction2(class extends Action2 { id: COPY_CELL_UP_COMMAND_ID, title: localize('notebookActions.copyCellUp', "Copy Cell Up"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true, keybinding: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow, @@ -1017,6 +1041,7 @@ registerAction2(class extends Action2 { id: COPY_CELL_DOWN_COMMAND_ID, title: localize('notebookActions.copyCellDown', "Copy Cell Down"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true, keybinding: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow, @@ -1074,7 +1099,7 @@ registerAction2(class extends Action2 { return; } - editor.focusNotebookCell(newCell, true); + editor.focusNotebookCell(newCell, 'editor'); } }); @@ -1119,10 +1144,73 @@ registerAction2(class extends Action2 { return; } - editor.focusNotebookCell(newCell, true); + editor.focusNotebookCell(newCell, 'editor'); } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: FOCUS_IN_OUTPUT_COMMAND_ID, + title: localize('focusOutput', 'Focus In Active Cell Output'), + category: NOTEBOOK_ACTIONS_CATEGORY, + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED), + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, + mac: { primary: KeyMod.WinCtrl | KeyCode.DownArrow, }, + weight: KeybindingWeight.WorkbenchContrib + }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + f1: true + }); + } + + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + if (!isCellActionContext(context)) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + const editor = context.notebookEditor; + const activeCell = context.cell; + editor.focusNotebookCell(activeCell, 'output'); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: FOCUS_OUT_OUTPUT_COMMAND_ID, + title: localize('focusOutputOut', 'Focus Out Active Cell Output'), + category: NOTEBOOK_ACTIONS_CATEGORY, + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED), + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + mac: { primary: KeyMod.WinCtrl | KeyCode.UpArrow, }, + weight: KeybindingWeight.WorkbenchContrib + }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + f1: true + }); + } + + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + if (!isCellActionContext(context)) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + const editor = context.notebookEditor; + const activeCell = context.cell; + editor.focusNotebookCell(activeCell, 'editor'); + } +}); + + registerAction2(class extends Action2 { constructor() { super({ @@ -1199,6 +1287,7 @@ registerAction2(class extends Action2 { mac: { primary: KeyMod.CtrlCmd | KeyCode.UpArrow }, weight: KeybindingWeight.WorkbenchContrib }, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), f1: true }); } @@ -1217,7 +1306,7 @@ registerAction2(class extends Action2 { } const firstCell = editor.viewModel.viewCells[0]; - editor.focusNotebookCell(firstCell, false); + editor.focusNotebookCell(firstCell, 'container'); } }); @@ -1233,6 +1322,7 @@ registerAction2(class extends Action2 { mac: { primary: KeyMod.CtrlCmd | KeyCode.DownArrow }, weight: KeybindingWeight.WorkbenchContrib }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } @@ -1251,7 +1341,7 @@ registerAction2(class extends Action2 { } const firstCell = editor.viewModel.viewCells[editor.viewModel.length - 1]; - editor.focusNotebookCell(firstCell, false); + editor.focusNotebookCell(firstCell, 'container'); } }); @@ -1267,6 +1357,7 @@ registerAction2(class extends Action2 { order: CellToolbarOrder.ClearCellOutput }, icon: { id: 'codicon/clear-all' }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } @@ -1299,6 +1390,7 @@ export class ChangeCellLanguageAction extends Action2 { id: CHANGE_CELL_LANGUAGE, title: localize('changeLanguage', 'Change Cell Language'), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } @@ -1366,7 +1458,7 @@ export class ChangeCellLanguageAction extends Action2 { if (selection.languageId === 'markdown' && context.cell?.language !== 'markdown') { const newCell = await changeCellToKind(CellKind.Markdown, { cell: context.cell, notebookEditor: context.notebookEditor }); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, true); + context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } else if (selection.languageId !== 'markdown' && context.cell?.language === 'markdown') { await changeCellToKind(CellKind.Code, { cell: context.cell, notebookEditor: context.notebookEditor }, selection.languageId); @@ -1410,6 +1502,7 @@ registerAction2(class extends Action2 { order: 0 }, icon: { id: 'codicon/clear-all' }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } @@ -1435,7 +1528,7 @@ async function splitCell(context: INotebookCellActionContext): Promise { if (context.cell.cellKind === CellKind.Code) { const newCells = await context.notebookEditor.splitNotebookCell(context.cell); if (newCells) { - context.notebookEditor.focusNotebookCell(newCells[newCells.length - 1], true); + context.notebookEditor.focusNotebookCell(newCells[newCells.length - 1], 'editor'); } } } @@ -1453,6 +1546,7 @@ registerAction2(class extends Action2 { order: CellToolbarOrder.SplitCell }, icon: { id: 'codicon/split-vertical' }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } @@ -1473,7 +1567,7 @@ registerAction2(class extends Action2 { async function joinCells(context: INotebookCellActionContext, direction: 'above' | 'below'): Promise { const cell = await context.notebookEditor.joinNotebookCells(context.cell, direction, CellKind.Code); if (cell) { - context.notebookEditor.focusNotebookCell(cell, true); + context.notebookEditor.focusNotebookCell(cell, 'editor'); } } @@ -1484,6 +1578,7 @@ registerAction2(class extends Action2 { id: JOIN_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.joinCellAbove', "Join with Previous Cell"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } @@ -1507,6 +1602,7 @@ registerAction2(class extends Action2 { id: JOIN_CELL_BELOW_COMMAND_ID, title: localize('notebookActions.joinCellBelow', "Join with Next Cell"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts index 5f0605296dc..c3c16540d26 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { INotebookEditor, INotebookEditorMouseEvent, ICellRange, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, INotebookEditorMouseEvent, ICellRange, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as DOM from 'vs/base/browser/dom'; import { CellFoldingState, FoldingModel } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -141,6 +141,7 @@ registerAction2(class extends Action2 { primary: KeyCode.LeftArrow, weight: KeybindingWeight.WorkbenchContrib }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } @@ -179,6 +180,7 @@ registerAction2(class extends Action2 { primary: KeyCode.RightArrow, weight: KeybindingWeight.WorkbenchContrib }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts new file mode 100644 index 00000000000..6d7d9c8a64f --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { IMarkerListProvider, MarkerList, IMarkerNavigationService } from 'vs/editor/contrib/gotoError/markerNavigationService'; +import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +class MarkerListProvider implements IMarkerListProvider { + + private readonly _dispoables: IDisposable; + + constructor( + @IMarkerService private readonly _markerService: IMarkerService, + @IMarkerNavigationService markerNavigation: IMarkerNavigationService, + ) { + this._dispoables = markerNavigation.registerProvider(this); + } + + dispose() { + this._dispoables.dispose(); + } + + getMarkerList(resource: URI | undefined): MarkerList | undefined { + if (!resource) { + return undefined; + } + const data = CellUri.parse(resource); + if (!data) { + return undefined; + } + return new MarkerList(uri => { + const otherData = CellUri.parse(uri); + return otherData?.notebook.toString() === data.notebook.toString(); + }, this._markerService); + } +} + +Registry + .as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(MarkerListProvider, LifecyclePhase.Ready); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts index 838ffdd7926..d7305d9e53f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts @@ -14,18 +14,24 @@ TableOfContentsProviderRegistry.register(NotebookEditor.ID, new class implements return undefined; } // return an entry per markdown header + const editorWidget = editor.getControl(); const result: ITableOfContentsEntry[] = []; for (let cell of editor.viewModel.viewCells) { - if (cell.cellKind === CellKind.Code) { - continue; - } const content = cell.getText(); - const matches = content.match(/^[ \t]*(\#+)(.+)$/gm); + const regexp = cell.cellKind === CellKind.Markdown + ? /^[ \t]*(\#+)(.+)$/gm // md: header + : /^.*\w+.*\w*$/m; // code: none empty line + + const matches = content.match(regexp); if (matches && matches.length) { for (let j = 0; j < matches.length; j++) { result.push({ label: matches[j].replace(/^[ \t]*(\#+)/, ''), - reveal: () => editor.revealInCenterIfOutsideViewport(cell) + reveal: () => { + editorWidget.revealInCenterIfOutsideViewport(cell); + editorWidget.selectElement(cell); + // editor.focusNotebookCell(cell, 'container'); + } }); } } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 1a7895cefd8..5666ff053b8 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .part.editor > .content .notebook-editor { +.monaco-workbench .notebookOverlay.notebook-editor { box-sizing: border-box; line-height: 22px; user-select: initial; @@ -17,40 +17,40 @@ white-space: initial; } -/* .monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element { - overflow: visible !important; -} */ +.notebookOverlay .simple-fr-find-part-wrapper.visible { + z-index: 100; +} -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .overflowingContentWidgets > div { +.notebookOverlay .cell-list-container .overflowingContentWidgets > div { z-index: 600 !important; /* @rebornix: larger than the editor title bar */ } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .monaco-list-rows { +.notebookOverlay .cell-list-container .monaco-list-rows { min-height: 100%; overflow: visible !important; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container { +.notebookOverlay .cell-list-container { position: relative; } -.monaco-workbench .part.editor > .content .notebook-editor.global-drag-active .webview { +.notebookOverlay.global-drag-active .webview { pointer-events: none; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .webview-cover { +.notebookOverlay .cell-list-container .webview-cover { position: absolute; top: 0; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { cursor: default; overflow: visible !important; width: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image { position: absolute; top: -500px; z-index: 1000; @@ -58,55 +58,55 @@ padding-top: 8px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .notebook-cell-focus-indicator { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .notebook-cell-focus-indicator { top: 8px !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .notebook-cell-focus-indicator { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .notebook-cell-focus-indicator { bottom: 8px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .output { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .output { display: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image > .monaco-toolbar { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image > .monaco-toolbar { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-statusbar-container { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-statusbar-container { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-part { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-part { width: calc(100% - 32px); /* minus left gutter */ } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-container > div > div { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-container > div > div { /* Rendered code content - show a single unwrapped line */ height: 20px; overflow: hidden; white-space: pre-wrap; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .cell.markdown { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .cell.markdown { white-space: nowrap; overflow: hidden; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell { display: flex; } -.monaco-workbench .part.editor > .content .notebook-editor .notebook-content-widgets { +.notebookOverlay .notebook-content-widgets { position: absolute; top: 0; left: 0; width: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor .output { +.notebookOverlay .output { padding-left: 8px; padding-right: 8px; user-select: text; @@ -115,22 +115,22 @@ box-sizing: border-box; } -.monaco-workbench .part.editor > .content .notebook-editor .output p { +.notebookOverlay .output p { white-space: initial; overflow-x: auto; margin: 0px; } -.monaco-workbench .part.editor > .content .notebook-editor .output > div.foreground { +.notebookOverlay .output > div.foreground { padding: 8px; box-sizing: border-box; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-drag-image .output .multi-mimetype-output { +.notebookOverlay .cell-drag-image .output .multi-mimetype-output { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor .output .multi-mimetype-output { +.notebookOverlay .output .multi-mimetype-output { position: absolute; top: 4px; left: -32px; @@ -139,27 +139,27 @@ cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error_message { +.notebookOverlay .output .error_message { color: red; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error > div { +.notebookOverlay .output .error > div { white-space: normal; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error pre.traceback { +.notebookOverlay .output .error pre.traceback { margin: 8px 0; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error .traceback > span { +.notebookOverlay .output .error .traceback > span { display: block; } -.monaco-workbench .part.editor > .content .notebook-editor .output .display img { +.notebookOverlay .output .display img { max-width: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu { position: absolute; left: 0; top: 28px; @@ -170,33 +170,29 @@ } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .menu, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .menu { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .menu, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .menu { visibility: visible; } -.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:hover, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover { outline: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu:hover { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu:hover { cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element { - padding-top: 16px; -} - -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { visibility: hidden; display: inline-block; position: absolute; @@ -207,7 +203,7 @@ z-index: 30; } -.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 { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item { width: 24px; height: 24px; display: flex; @@ -215,55 +211,55 @@ 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 { +.notebookOverlay > .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; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container { +.notebookOverlay .cell-statusbar-container { height: 21px; font-size: 12px; display: flex; position: relative; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-status-left { +.notebookOverlay .cell-statusbar-container .cell-status-left { display: flex; flex-grow: 1; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-status-right { +.notebookOverlay .cell-statusbar-container .cell-status-right { padding-right: 12px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker { +.notebookOverlay .cell-statusbar-container .cell-language-picker { padding: 0px 6px; cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:hover { +.notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: rgba(255, 255, 255, 0.6); } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:hover { +.notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: rgba(255, 255, 255, 0.9); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:hover { +.vs-dark .notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: rgba(255, 255, 255, 0.15); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:active { +.vs-dark .notebookOverlay .cell-statusbar-container .cell-language-picker:active { background-color: rgba(255, 255, 255, 0.2); } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-status-message { +.notebookOverlay .cell-statusbar-container .cell-status-message { display: flex; align-items: center; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-run-status { +.notebookOverlay .cell-statusbar-container .cell-run-status { height: 100%; display: flex; align-items: center; @@ -272,19 +268,19 @@ margin-right: 2px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .codicon { +.notebookOverlay .cell-statusbar-container .codicon { font-size: 14px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-run-status .codicon-check { +.notebookOverlay .cell-statusbar-container .cell-run-status .codicon-check { color: #89D185; } -.vs .monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-run-status .codicon-check { +.vs .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-check { color: #388A34; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-status-placeholder { +.notebookOverlay .cell-status-placeholder { position: absolute; left: 18px; color: #ccc9; @@ -294,32 +290,32 @@ top: 0px; } -.vs .monaco-workbench .part.editor > .content .notebook-editor .cell-status-placeholder { +.vs .notebookOverlay .cell-status-placeholder { color: #616161e6; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { position: relative; height: 22px; flex-shrink: 0; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar { margin-top: 8px; visibility: hidden; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .codicon { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .codicon { margin-right: 8px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell.runnable .run-button-container .monaco-toolbar { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell.runnable .run-button-container .monaco-toolbar, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell.runnable .run-button-container .monaco-toolbar, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell.runnable .run-button-container .monaco-toolbar { visibility: visible; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .execution-count-label { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .execution-count-label { position: absolute; top: 2px; font-size: 10px; @@ -333,33 +329,42 @@ opacity: .6; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell .run-button-container .execution-count-label, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell .run-button-container .execution-count-label, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell .run-button-container .execution-count-label { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell .run-button-container .execution-count-label, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell .run-button-container .execution-count-label, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell .run-button-container .execution-count-label { visibility: hidden; } -.monaco-workbench .part.editor > .content .notebook-editor .cell .cell-editor-part { +.notebookOverlay .cell .cell-editor-part { position: relative; } -.monaco-workbench .part.editor > .content .notebook-editor .cell .monaco-progress-container { +.notebookOverlay .cell .monaco-progress-container { top: -5px; + + position: absolute; + left: 0; + z-index: 5; + height: 2px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.focused > .monaco-toolbar, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.cell-output-hover > .monaco-toolbar, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions:hover > .monaco-toolbar { +.notebookOverlay .cell .monaco-progress-container .progress-bit { + height: 2px; +} + +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.focused > .monaco-toolbar, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.cell-output-hover > .monaco-toolbar, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions:hover > .monaco-toolbar { visibility: visible; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list:not(.element-focused):focus:before { +.notebookOverlay > .cell-list-container > .monaco-list:not(.element-focused):focus:before { outline: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .notebook-cell-focus-indicator { +.notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator { display: block; content: ' '; position: absolute; @@ -373,13 +378,13 @@ cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row:hover .notebook-cell-focus-indicator, -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator, -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.focused .notebook-cell-focus-indicator { +.notebookOverlay .monaco-list .monaco-list-row:hover .notebook-cell-focus-indicator, +.notebookOverlay .monaco-list .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator, +.notebookOverlay .monaco-list .monaco-list-row.focused .notebook-cell-focus-indicator { visibility: visible; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-editor-focus .cell-editor-part:before { +.notebookOverlay .monaco-list-row.cell-editor-focus .cell-editor-part:before { z-index: 20; content: ""; right: 0px; @@ -392,31 +397,25 @@ pointer-events: none; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator-top { +.notebookOverlay .monaco-list .monaco-list-row .cell-insertion-indicator-top { top: -15px; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator-bottom { - bottom: 13px; -} - -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-dragover-top .cell-insertion-indicator-top, -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-dragover-bottom .cell-insertion-indicator-bottom { - opacity: 1; -} - -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator { - opacity: 0; - transition: opacity 0.2s ease-in-out; +.notebookOverlay > .cell-list-container > .cell-list-insertion-indicator { position: absolute; height: 2px; + left: 0px; + right: 0px; + opacity: 0; + /* transition: opacity 0.2s ease-in-out; */ + z-index: 10; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-dragging { - opacity: 0.5; +.notebookOverlay > .cell-list-container > .monaco-list .monaco-list-row.cell-dragging { + opacity: 0.5 !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { position: absolute; display: flex; opacity: 0; @@ -425,28 +424,28 @@ padding: 0; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-bottom-toolbar-container { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-bottom-toolbar-container { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within, -.monaco-workbench .part.editor > .content .notebook-editor.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { +.notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within, +.notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { opacity: 1; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator { height: 1px; flex-grow: 1; align-self: center; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator-short { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator-short { height: 1px; width: 16px; align-self: center; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .button { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .button { display: flex; margin: 0 8px; padding: 0 8px; @@ -457,7 +456,7 @@ font-size: 12px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { text-align: center; font-size: 14px; color: inherit; @@ -466,34 +465,34 @@ /* markdown */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown img { +.notebookOverlay .cell.markdown img { max-width: 100%; max-height: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a { +.notebookOverlay .cell.markdown a { text-decoration: none; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a:hover { +.notebookOverlay .cell.markdown a:hover { text-decoration: underline; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a:focus, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown input:focus, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown select:focus, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown textarea:focus { +.notebookOverlay .cell.markdown a:focus, +.notebookOverlay .cell.markdown input:focus, +.notebookOverlay .cell.markdown select:focus, +.notebookOverlay .cell.markdown textarea:focus { outline: 1px solid -webkit-focus-ring-color; outline-offset: -1px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr { +.notebookOverlay .cell.markdown hr { border: 0; height: 2px; border-bottom: 2px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 { +.notebookOverlay .cell.markdown h1 { padding-bottom: 0.3em; line-height: 1.2; border-bottom-width: 1px; @@ -501,116 +500,116 @@ border-color: rgba(255, 255, 255, 0.18); } -.vs .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 { +.vs .notebookOverlay .cell.markdown h1 { border-color: rgba(0, 0, 0, 0.18); } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h2, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h3 { +.notebookOverlay .cell.markdown h1, +.notebookOverlay .cell.markdown h2, +.notebookOverlay .cell.markdown h3 { font-weight: normal; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div { +.notebookOverlay .cell.markdown div { width: 100%; } /* Adjust margin of first item in markdown cell */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div *:first-child { +.notebookOverlay .cell.markdown div *:first-child { margin-top: 4px; } /* h1 tags don't need top margin */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div h1:first-child { +.notebookOverlay .cell.markdown div h1:first-child { margin-top: 0; } /* Removes bottom margin when only one item exists in markdown cell */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div *:only-child, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div *:last-child { +.notebookOverlay .cell.markdown div *:only-child, +.notebookOverlay .cell.markdown div *:last-child { margin-bottom: 0; } /* makes all markdown cells consistent */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div { +.notebookOverlay .cell.markdown div { min-height: 32px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table { +.notebookOverlay .cell.markdown table { border-collapse: collapse; border-spacing: 0; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table th, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table td { +.notebookOverlay .cell.markdown table th, +.notebookOverlay .cell.markdown table td { border: 1px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { +.notebookOverlay .cell.markdown table > thead > tr > th { text-align: left; border-bottom: 1px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > td, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > th, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { +.notebookOverlay .cell.markdown table > thead > tr > th, +.notebookOverlay .cell.markdown table > thead > tr > td, +.notebookOverlay .cell.markdown table > tbody > tr > th, +.notebookOverlay .cell.markdown table > tbody > tr > td { padding: 5px 10px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr + tr > td { +.notebookOverlay .cell.markdown table > tbody > tr + tr > td { border-top: 1px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown blockquote { +.notebookOverlay .cell.markdown blockquote { margin: 0 7px 0 5px; padding: 0 16px 0 10px; border-left-width: 5px; border-left-style: solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown code { +.notebookOverlay .cell.markdown code { font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; font-size: 1em; line-height: 1.357em; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown body.wordWrap pre { +.notebookOverlay .cell.markdown body.wordWrap pre { white-space: pre-wrap; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre:not(.hljs), -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre.hljs code > div { +.notebookOverlay .cell.markdown pre:not(.hljs), +.notebookOverlay .cell.markdown pre.hljs code > div { padding: 16px; border-radius: 3px; overflow: auto; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre code { +.notebookOverlay .cell.markdown pre code { color: var(--vscode-editor-foreground); tab-size: 4; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block { +.notebookOverlay .cell.markdown .latex-block { display: block; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex { +.notebookOverlay .cell.markdown .latex { vertical-align: middle; display: inline-block; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex img, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block img { +.notebookOverlay .cell.markdown .latex img, +.notebookOverlay .cell.markdown .latex-block img { filter: brightness(0) invert(0) } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex img, -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block img { +.vs-dark .notebookOverlay .cell.markdown .latex img, +.vs-dark .notebookOverlay .cell.markdown .latex-block img { filter: brightness(0) invert(1) } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container .notebook-folding-indicator { +.notebookOverlay > .cell-list-container .notebook-folding-indicator { position: absolute; top: 8px; left: 6px; @@ -619,38 +618,38 @@ /** Theming */ -/* .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { +/* .notebookOverlay .cell.markdown pre { background-color: rgba(220, 220, 220, 0.4); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { +.vs-dark .notebookOverlay .cell.markdown pre { background-color: rgba(10, 10, 10, 0.4); } -.hc-black .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { +.hc-black .notebookOverlay .cell.markdown pre { background-color: rgb(0, 0, 0); } -.hc-black .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 { +.hc-black .notebookOverlay .cell.markdown h1 { border-color: rgb(0, 0, 0); } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { +.notebookOverlay .cell.markdown table > thead > tr > th { border-color: rgba(0, 0, 0, 0.18); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { +.vs-dark .notebookOverlay .cell.markdown table > thead > tr > th { border-color: rgba(255, 255, 255, 0.18); } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { +.notebookOverlay .cell.markdown h1, +.notebookOverlay .cell.markdown hr, +.notebookOverlay .cell.markdown table > tbody > tr > td { border-color: rgba(0, 0, 0, 0.18); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr, -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { +.vs-dark .notebookOverlay .cell.markdown h1, +.vs-dark .notebookOverlay .cell.markdown hr, +.vs-dark .notebookOverlay .cell.markdown table > tbody > tr > td { border-color: rgba(255, 255, 255, 0.18); } */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index a726ac0d66a..01319fab3ff 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { parse } from 'vs/base/common/marshalling'; import { basename, isEqual } from 'vs/base/common/resources'; @@ -24,7 +24,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { EditorInput, Extensions as EditorInputExtensions, IEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; -import { NotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; @@ -44,12 +44,15 @@ import 'vs/workbench/contrib/notebook/browser/contrib/find/findController'; import 'vs/workbench/contrib/notebook/browser/contrib/fold/folding'; import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; import 'vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider'; +import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; // Output renderers registration import 'vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform'; import 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform'; import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform'; +import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; /*--------------------------------------------------------------------------------------------- */ @@ -76,10 +79,11 @@ Registry.as(EditorInputExtensions.EditorInputFactor resource: input.resource, name: input.name, viewType: input.viewType, + group: input.group }); } deserialize(instantiationService: IInstantiationService, raw: string) { - type Data = { resource: URI, name: string, viewType: string }; + type Data = { resource: URI, name: string, viewType: string, group: number }; const data = parse(raw); if (!data) { return undefined; @@ -88,7 +92,13 @@ Registry.as(EditorInputExtensions.EditorInputFactor if (!data || !URI.isUri(resource) || typeof name !== 'string' || typeof viewType !== 'string') { return undefined; } - return NotebookEditorInput.getOrCreate(instantiationService, resource, name, viewType); + + const input = NotebookEditorInput.getOrCreate(instantiationService, resource, name, viewType); + if (typeof data.group === 'number') { + input.updateGroup(data.group); + } + + return input; } } ); @@ -97,17 +107,19 @@ function getFirstNotebookInfo(notebookService: INotebookService, uri: URI): Note return notebookService.getContributedNotebookProviders(uri)[0]; } -export class NotebookContribution implements IWorkbenchContribution { +export class NotebookContribution extends Disposable implements IWorkbenchContribution { private _resourceMapping = new ResourceMap(); constructor( - @IEditorService private readonly editorService: IEditorService, + @IEditorService private readonly editorService: EditorServiceImpl, @INotebookService private readonly notebookService: INotebookService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - this.editorService.overrideOpenEditor({ + super(); + + this._register(this.editorService.overrideOpenEditor({ getEditorOverrides: (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined) => { const currentEditorForResource = group?.editors.find(editor => isEqual(editor.resource, resource)); @@ -126,14 +138,24 @@ export class NotebookContribution implements IWorkbenchContribution { }); }, open: (editor, options, group, id) => this.onEditorOpening(editor, options, group, id) - }); + })); - this.editorService.onDidActiveEditorChange(() => { + this._register(this.editorService.onDidActiveEditorChange(() => { if (this.editorService.activeEditor && this.editorService.activeEditor! instanceof NotebookEditorInput) { let editorInput = this.editorService.activeEditor! as NotebookEditorInput; this.notebookService.updateActiveNotebookDocument(editorInput.viewType!, editorInput.resource!); } - }); + })); + + this._register(this.editorService.onDidCloseEditor(({ editor }) => { + if (!(editor instanceof NotebookEditorInput)) { + return; + } + + if (!this.editorService.editors.some(other => other === editor)) { + editor.dispose(); + } + })); } getUserAssociatedEditors(resource: URI) { @@ -156,6 +178,23 @@ export class NotebookContribution implements IWorkbenchContribution { } private onEditorOpening(originalInput: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup, id: string | undefined): IOpenEditorOverride | undefined { + if (originalInput instanceof NotebookEditorInput) { + if ((originalInput.group === group.id || originalInput.group === undefined) && (originalInput.viewType === id || typeof id !== 'string')) { + // No need to do anything + originalInput.updateGroup(group.id); + return undefined; + } else { + // Create a copy of the input. + // Unlike normal editor inputs, we do not want to share custom editor inputs + // between multiple editors / groups. + const copiedInput = this.instantiationService.createInstance(NotebookEditorInput, originalInput.resource, originalInput.name, originalInput.viewType); + copiedInput.updateGroup(group.id); + return { + override: this.editorService.openEditor(copiedInput, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group) + }; + } + } + let resource = originalInput.resource; if (!resource) { return undefined; @@ -181,6 +220,7 @@ export class NotebookContribution implements IWorkbenchContribution { const input = this._resourceMapping.get(resource); if (!input!.isDisposed()) { + input?.updateGroup(group.id); return { override: this.editorService.openEditor(input!, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group) }; } } @@ -199,6 +239,8 @@ export class NotebookContribution implements IWorkbenchContribution { input = NotebookEditorInput.getOrCreate(this.instantiationService, data.notebook, name, info.id); this._resourceMapping.set(data.notebook, input); } + + input.updateGroup(group.id); return { override: this.editorService.openEditor(input, new NotebookEditorOptions({ ...options, forceReload: true, cellOptions: { resource, options } }), group) }; } } @@ -211,6 +253,7 @@ export class NotebookContribution implements IWorkbenchContribution { } const input = NotebookEditorInput.getOrCreate(this.instantiationService, resource, originalInput.getName(), info.id); + input.updateGroup(group.id); this._resourceMapping.set(resource, input); return { override: this.editorService.openEditor(input, options, group) }; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 8fd1f41dfc6..bd2e616a317 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -15,16 +15,17 @@ import { ScrollEvent } from 'vs/base/common/scrollable'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { Range } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; -import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { Range } from 'vs/editor/common/core/range'; import { FindMatch, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; +import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { CellLanguageStatusBarItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, IOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; +import { ICompositeCodeEditor } from 'vs/editor/common/editorCommon'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); @@ -110,7 +111,6 @@ export interface ICellViewModel { resolveTextModel(): Promise; getEvaluatedMetadata(documentMetadata: NotebookDocumentMetadata | undefined): NotebookCellMetadata; getSelectionsStartPosition(): IPosition[] | undefined; - getLinesContent(): string[]; } export interface IEditableCellViewModel extends ICellViewModel { @@ -137,7 +137,7 @@ export interface INotebookEditorContribution { restoreViewState?(state: any): void; } -export interface INotebookEditor { +export interface INotebookEditor extends ICompositeCodeEditor { /** * Notebook view model attached to the current editor @@ -224,7 +224,7 @@ export interface INotebookEditor { /** * Focus the container of a cell (the monaco editor inside is not focused). */ - focusNotebookCell(cell: ICellViewModel, focusEditor: boolean): void; + focusNotebookCell(cell: ICellViewModel, focus: 'editor' | 'container' | 'output'): void; /** * Execute the given notebook cell @@ -355,6 +355,8 @@ export interface INotebookEditor { } export interface INotebookCellList { + elementAt(position: number): ICellViewModel; + elementHeight(element: ICellViewModel): number; onWillScroll: Event; onDidChangeFocus: Event>; onDidChangeContentHeight: Event; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 978435b1151..815c2e919fc 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -3,143 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/notebook'; -import { getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; -import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Color, RGBA } from 'vs/base/common/color'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, MutableDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { Range } from 'vs/editor/common/core/range'; -import { ICompositeCodeEditor, IEditor } from 'vs/editor/common/editorCommon'; -import * as nls from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { MutableDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; 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, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookEditor, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, INotebookEditorContribution, NOTEBOOK_EDITOR_RUNNABLE, IEditableCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; -import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; -import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; -import { CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate, CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; -import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, CellUri, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; -import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; +import { INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IPosition, Position } from 'vs/editor/common/core/position'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -const $ = DOM.$; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; -export class NotebookEditorOptions extends EditorOptions { - - readonly cellOptions?: IResourceEditorInput; - - constructor(options: Partial) { - super(); - this.overwrite(options); - this.cellOptions = options.cellOptions; - } - - with(options: Partial): NotebookEditorOptions { - return new NotebookEditorOptions({ ...this, ...options }); - } -} - -export class NotebookCodeEditors implements ICompositeCodeEditor { - - private readonly _disposables = new DisposableStore(); - private readonly _onDidChangeActiveEditor = new Emitter(); - readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; - - constructor( - private _list: INotebookCellList, - private _renderedEditors: Map - ) { - _list.onDidChangeFocus(_e => this._onDidChangeActiveEditor.fire(this), undefined, this._disposables); - } - - dispose(): void { - this._onDidChangeActiveEditor.dispose(); - this._disposables.dispose(); - } - - get activeCodeEditor(): IEditor | undefined { - const [focused] = this._list.getFocusedElements(); - return this._renderedEditors.get(focused); - } -} - -export class NotebookEditor extends BaseEditor implements INotebookEditor { +export class NotebookEditor extends BaseEditor { static readonly ID: string = 'workbench.editor.notebook'; - private _rootElement!: HTMLElement; - private body!: HTMLElement; - private titleBar: HTMLElement | null = null; - private webview: BackLayerWebView | null = null; - private webviewTransparentCover: HTMLElement | null = null; - private list: INotebookCellList | undefined; - private control: ICompositeCodeEditor | undefined; - private renderedEditors: Map = new Map(); - private eventDispatcher: NotebookEventDispatcher | undefined; - private notebookViewModel: NotebookViewModel | undefined; - private localStore: DisposableStore = this._register(new DisposableStore()); private editorMemento: IEditorMemento; private readonly groupListener = this._register(new MutableDisposable()); - private fontInfo: BareFontInfo | undefined; - private dimension: DOM.Dimension | null = null; - private editorFocus: IContextKey | null = null; - private editorEditable: IContextKey | null = null; - private editorRunnable: IContextKey | null = null; - private editorExecutingNotebook: IContextKey | null = null; - private outputRenderer: OutputRenderer; - protected readonly _contributions: { [key: string]: INotebookEditorContribution; }; - private scrollBeyondLastLine: boolean; + private _widget: NotebookEditorWidget; constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, - @INotebookService private notebookService: INotebookService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService - ) { + @IEditorGroupsService editorGroupService: IEditorGroupsService) { super(NotebookEditor.ID, telemetryService, themeService, storageService); + this._widget = this.instantiationService.createInstance(NotebookEditorWidget); this.editorMemento = this.getEditorMemento(editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); - this.outputRenderer = new OutputRenderer(this, this.instantiationService); - this._contributions = {}; - this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); - - this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('editor.scrollBeyondLastLine')) { - this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); - if (this.dimension) { - this.layout(this.dimension); - } - } - }); } private readonly _onDidChangeModel = new Emitter(); @@ -147,12 +44,12 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { set viewModel(newModel: NotebookViewModel | undefined) { - this.notebookViewModel = newModel; + this._widget.viewModel = newModel; this._onDidChangeModel.fire(); } get viewModel() { - return this.notebookViewModel; + return this._widget.viewModel; } get minimumWidth(): number { return 375; } @@ -170,207 +67,18 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return true; } - private updateEditorFocus() { - // Note - focus going to the webview will fire 'blur', but the webview element will be - // a descendent of the notebook editor root. - this.editorFocus?.set(DOM.isAncestor(document.activeElement, this.getDomNode())); - } - protected createEditor(parent: HTMLElement): void { - this._rootElement = DOM.append(parent, $('.notebook-editor')); - this.createBody(this._rootElement); - this.generateFontInfo(); - this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService); - this.editorFocus.set(true); - this._register(this.onDidFocus(() => this.updateEditorFocus())); - this._register(this.onDidBlur(() => this.updateEditorFocus())); - - this.editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.contextKeyService); - this.editorEditable.set(true); - this.editorRunnable = NOTEBOOK_EDITOR_RUNNABLE.bindTo(this.contextKeyService); - this.editorRunnable.set(true); - this.editorExecutingNotebook = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.contextKeyService); - - const contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); - - for (const desc of contributions) { - try { - const contribution = this.instantiationService.createInstance(desc.ctor, this); - this._contributions[desc.id] = contribution; - } catch (err) { - onUnexpectedError(err); - } - } - } - - populateEditorTitlebar() { - for (let element: HTMLElement | null = this._rootElement.parentElement; element; element = element.parentElement) { - if (DOM.hasClass(element, 'editor-group-container')) { - // elemnet is editor group container - for (let i = 0; i < element.childElementCount; i++) { - const child = element.childNodes.item(i) as HTMLElement; - - if (DOM.hasClass(child, 'title')) { - this.titleBar = child; - break; - } - } - break; - } - } - } - - clearEditorTitlebarZindex() { - if (this.titleBar === null) { - this.populateEditorTitlebar(); - } - - if (this.titleBar) { - this.titleBar.style.zIndex = 'auto'; - } - } - - increaseEditorTitlebarZindex() { - if (this.titleBar === null) { - this.populateEditorTitlebar(); - } - - if (this.titleBar) { - this.titleBar.style.zIndex = '500'; - } - } - - private generateFontInfo(): void { - const editorOptions = this.configurationService.getValue('editor'); - this.fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); - } - - private createBody(parent: HTMLElement): void { - this.body = document.createElement('div'); - DOM.addClass(this.body, 'cell-list-container'); - this.createCellList(); - DOM.append(parent, this.body); - } - - private createCellList(): void { - DOM.addClass(this.body, 'cell-list-container'); - - const dndController = this._register(new CellDragAndDropController(this)); - const renders = [ - this.instantiationService.createInstance(CodeCellRenderer, this, this.renderedEditors, dndController), - this.instantiationService.createInstance(MarkdownCellRenderer, this.contextKeyService, this, dndController, this.renderedEditors), - ]; - - this.list = this.instantiationService.createInstance( - NotebookCellList, - 'NotebookCellList', - this.body, - this.instantiationService.createInstance(NotebookCellListDelegate), - renders, - this.contextKeyService, - { - setRowLineHeight: false, - setRowHeight: false, - supportDynamicHeights: true, - horizontalScrolling: false, - keyboardSupport: false, - mouseSupport: true, - multipleSelectionSupport: false, - enableKeyboardNavigation: true, - additionalScrollHeight: 0, - transformOptimization: false, - styleController: (_suffix: string) => { return this.list!; }, - overrideStyles: { - listBackground: editorBackground, - listActiveSelectionBackground: editorBackground, - listActiveSelectionForeground: foreground, - listFocusAndSelectionBackground: editorBackground, - listFocusAndSelectionForeground: foreground, - listFocusBackground: editorBackground, - listFocusForeground: foreground, - listHoverForeground: foreground, - listHoverBackground: editorBackground, - listHoverOutline: focusBorder, - listFocusOutline: focusBorder, - listInactiveSelectionBackground: editorBackground, - listInactiveSelectionForeground: foreground, - listInactiveFocusBackground: editorBackground, - listInactiveFocusOutline: editorBackground, - }, - accessibilityProvider: { - getAriaLabel() { return null; }, - getWidgetAriaLabel() { - return nls.localize('notebookTreeAriaLabel', "Notebook"); - } - } - }, - ); - - this.control = new NotebookCodeEditors(this.list, this.renderedEditors); - this.webview = this.instantiationService.createInstance(BackLayerWebView, this); - this.webview.webview.onDidBlur(() => this.updateEditorFocus()); - this.webview.webview.onDidFocus(() => this.updateEditorFocus()); - this._register(this.webview.onMessage(message => { - if (this.viewModel) { - this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.viewModel.uri, message); - } - })); - this.list.rowsContainer.appendChild(this.webview.element); - - this._register(this.list); - this._register(combinedDisposable(...renders)); - - // transparent cover - this.webviewTransparentCover = DOM.append(this.list.rowsContainer, $('.webview-cover')); - this.webviewTransparentCover.style.display = 'none'; - - this._register(DOM.addStandardDisposableGenericMouseDownListner(this._rootElement, (e: StandardMouseEvent) => { - if (DOM.hasClass(e.target, 'slider') && this.webviewTransparentCover) { - this.webviewTransparentCover.style.display = 'block'; - } - })); - - this._register(DOM.addStandardDisposableGenericMouseUpListner(this._rootElement, (e: StandardMouseEvent) => { - if (this.webviewTransparentCover) { - // no matter when - this.webviewTransparentCover.style.display = 'none'; - } - })); - - this._register(this.list.onMouseDown(e => { - if (e.element) { - this._onMouseDown.fire({ event: e.browserEvent, target: e.element }); - } - })); - - this._register(this.list.onMouseUp(e => { - if (e.element) { - this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); - } - })); - + this._widget.createEditor(parent); + this._register(this.onDidFocus(() => this._widget.updateEditorFocus())); + this._register(this.onDidBlur(() => this._widget.updateEditorFocus())); } getDomNode() { - return this._rootElement; + return this._widget.getShadowDomNode(); } getControl() { - return this.control; - } - - getInnerWebview(): Webview | undefined { - return this.webview?.webview; - } - - setVisible(visible: boolean, group?: IEditorGroup): void { - if (visible) { - this.increaseEditorTitlebarZindex(); - } else { - this.clearEditorTitlebarZindex(); - } - - super.setVisible(visible, group); + return this._widget; } onWillHide() { @@ -378,15 +86,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.saveEditorViewState(this.input); } - this.editorFocus?.set(false); - if (this.webview) { - this.localStore.clear(); - this.list?.rowsContainer.removeChild(this.webview?.element); - this.webview?.dispose(); - this.webview = null; - } - - this.list?.clear(); + this._widget.onWillHide(); super.onHide(); } @@ -402,15 +102,13 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } if (editor === this.input) { - this.clearEditorTitlebarZindex(); this.saveEditorViewState(editor); } } focus() { super.focus(); - this.editorFocus?.set(true); - this.list?.domFocus(); + this._widget.focus(); } async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { @@ -421,225 +119,17 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { await super.setInput(input, options, token); const model = await input.resolve(); - if (this.notebookViewModel === undefined || !this.notebookViewModel.equal(model.notebook) || this.webview === null) { - this.detachModel(); - await this.attachModel(input, model); - } - - // reveal cell if editor options tell to do so - if (options instanceof NotebookEditorOptions && options.cellOptions) { - const cellOptions = options.cellOptions; - const cell = this.notebookViewModel!.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); - if (cell) { - this.selectElement(cell); - this.revealInCenterIfOutsideViewport(cell); - const editor = this.renderedEditors.get(cell)!; - if (editor) { - if (cellOptions.options?.selection) { - const { selection } = cellOptions.options; - editor.setSelection({ - ...selection, - endLineNumber: selection.endLineNumber || selection.startLineNumber, - endColumn: selection.endColumn || selection.startColumn - }); - } - if (!cellOptions.options?.preserveFocus) { - editor.focus(); - } - } - } - } + const viewState = this.loadTextEditorViewState(input); + this._widget.setModel(model, viewState, options); } clearInput(): void { super.clearInput(); } - private detachModel() { - this.localStore.clear(); - this.list?.detachViewModel(); - this.viewModel?.dispose(); - // avoid event - this.notebookViewModel = undefined; - this.webview?.clearInsets(); - this.webview?.clearPreloadsCache(); - this.list?.clear(); - } - - private updateForMetadata(): void { - this.editorEditable?.set(!!this.viewModel!.metadata?.editable); - this.editorRunnable?.set(!!this.viewModel!.metadata?.runnable); - DOM.toggleClass(this.getDomNode(), 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); - } - - private async attachModel(input: NotebookEditorInput, model: NotebookEditorModel) { - if (!this.webview) { - this.webview = this.instantiationService.createInstance(BackLayerWebView, this); - this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element); - } - - await this.webview.waitForInitialization(); - - this.eventDispatcher = new NotebookEventDispatcher(); - this.viewModel = this.instantiationService.createInstance(NotebookViewModel, input.viewType!, model.notebook, this.eventDispatcher, this.getLayoutInfo()); - this.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); - - this.updateForMetadata(); - this.localStore.add(this.eventDispatcher.onDidChangeMetadata((e) => { - this.updateForMetadata(); - })); - - // restore view states, including contributions - const viewState = this.loadTextEditorViewState(input); - - { - // restore view state - this.viewModel.restoreEditorViewState(viewState); - - // contribution state restore - - const contributionsState = viewState?.contributionsState || {}; - const keys = Object.keys(this._contributions); - for (let i = 0, len = keys.length; i < len; i++) { - const id = keys[i]; - const contribution = this._contributions[id]; - if (typeof contribution.restoreViewState === 'function') { - contribution.restoreViewState(contributionsState[id]); - } - } - } - - this.webview?.updateRendererPreloads(this.viewModel.renderers); - - this.localStore.add(this.list!.onWillScroll(e => { - this.webview!.updateViewScrollTop(-e.scrollTop, []); - this.webviewTransparentCover!.style.top = `${e.scrollTop}px`; - })); - - this.localStore.add(this.list!.onDidChangeContentHeight(() => { - DOM.scheduleAtNextAnimationFrame(() => { - const scrollTop = this.list?.scrollTop || 0; - const scrollHeight = this.list?.scrollHeight || 0; - this.webview!.element.style.height = `${scrollHeight}px`; - - if (this.webview?.insetMapping) { - let updateItems: { cell: CodeCellViewModel, output: IOutput, cellTop: number }[] = []; - let removedItems: IOutput[] = []; - this.webview?.insetMapping.forEach((value, key) => { - const cell = value.cell; - const viewIndex = this.list?.getViewIndex(cell); - - if (viewIndex === undefined) { - return; - } - - if (cell.outputs.indexOf(key) < 0) { - // output is already gone - removedItems.push(key); - } - - const cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; - if (this.webview!.shouldUpdateInset(cell, key, cellTop)) { - updateItems.push({ - cell: cell, - output: key, - cellTop: cellTop - }); - } - }); - - removedItems.forEach(output => this.webview?.removeInset(output)); - - if (updateItems.length) { - this.webview?.updateViewScrollTop(-scrollTop, updateItems); - } - } - }); - })); - - this.list!.attachViewModel(this.viewModel); - this.localStore.add(this.list!.onDidRemoveOutput(output => { - this.removeInset(output); - })); - this.localStore.add(this.list!.onDidHideOutput(output => { - this.hideInset(output); - })); - - this.list!.layout(); - - // restore list state at last, it must be after list layout - this.restoreListViewState(viewState); - } - - private restoreListViewState(viewState: INotebookEditorViewState | undefined): void { - if (viewState?.scrollPosition !== undefined) { - this.list!.scrollTop = viewState!.scrollPosition.top; - this.list!.scrollLeft = viewState!.scrollPosition.left; - } else { - this.list!.scrollTop = 0; - this.list!.scrollLeft = 0; - } - - const focusIdx = typeof viewState?.focus === 'number' ? viewState.focus : 0; - if (focusIdx < this.list!.length) { - this.list!.setFocus([focusIdx]); - this.list!.setSelection([focusIdx]); - } else if (this.list!.length > 0) { - this.list!.setFocus([0]); - } - - if (viewState?.editorFocused) { - this.list?.focusView(); - const cell = this.notebookViewModel?.viewCells[focusIdx]; - if (cell) { - cell.focusMode = CellFocusMode.Editor; - } - } - } - private saveEditorViewState(input: NotebookEditorInput): void { - if (this.group && this.notebookViewModel) { - const state = this.notebookViewModel.geteEditorViewState(); - if (this.list) { - state.scrollPosition = { left: this.list.scrollLeft, top: this.list.scrollTop }; - let cellHeights: { [key: number]: number } = {}; - for (let i = 0; i < this.viewModel!.length; i++) { - const elm = this.viewModel!.viewCells[i] as CellViewModel; - if (elm.cellKind === CellKind.Code) { - cellHeights[i] = elm.layoutInfo.totalHeight; - } else { - cellHeights[i] = 0; - } - } - - state.cellTotalHeights = cellHeights; - - const focus = this.list.getFocus()[0]; - if (focus) { - const element = this.notebookViewModel!.viewCells[focus]; - const itemDOM = this.list?.domElementOfElement(element!); - let editorFocused = false; - if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { - editorFocused = true; - } - - state.editorFocused = editorFocused; - state.focus = focus; - } - } - - // Save contribution view states - const contributionsState: { [key: string]: any } = {}; - - const keys = Object.keys(this._contributions); - for (const id of keys) { - const contribution = this._contributions[id]; - if (typeof contribution.saveViewState === 'function') { - contributionsState[id] = contribution.saveViewState(); - } - } - - state.contributionsState = contributionsState; + if (this.group) { + const state = this._widget.getEditorViewState(); this.editorMemento.saveEditorState(this.group, input.resource, state); } } @@ -653,19 +143,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } layout(dimension: DOM.Dimension): void { - this.dimension = new DOM.Dimension(dimension.width, dimension.height); - DOM.toggleClass(this._rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600); - DOM.toggleClass(this._rootElement, 'narrow-width', dimension.width < 600); - DOM.size(this.body, dimension.width, dimension.height); - this.list?.updateOptions({ additionalScrollHeight: this.scrollBeyondLastLine ? dimension.height : 0 }); - this.list?.layout(dimension.height, dimension.width); - - if (this.webviewTransparentCover) { - this.webviewTransparentCover.style.height = `${dimension.height}px`; - this.webviewTransparentCover.style.width = `${dimension.width}px`; - } - - this.eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + this._widget.layout(dimension); } protected saveState(): void { @@ -680,546 +158,10 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { //#region Editor Features - selectElement(cell: ICellViewModel) { - this.list?.selectElement(cell); - // this.viewModel!.selectionHandles = [cell.handle]; - } - - revealInView(cell: ICellViewModel) { - this.list?.revealElementInView(cell); - } - - revealInCenterIfOutsideViewport(cell: ICellViewModel) { - this.list?.revealElementInCenterIfOutsideViewport(cell); - } - - revealInCenter(cell: ICellViewModel) { - this.list?.revealElementInCenter(cell); - } - - revealLineInView(cell: ICellViewModel, line: number): void { - this.list?.revealElementLineInView(cell, line); - } - - revealLineInCenter(cell: ICellViewModel, line: number) { - this.list?.revealElementLineInCenter(cell, line); - } - - revealLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number) { - this.list?.revealElementLineInCenterIfOutsideViewport(cell, line); - } - - revealRangeInView(cell: ICellViewModel, range: Range): void { - this.list?.revealElementRangeInView(cell, range); - } - - revealRangeInCenter(cell: ICellViewModel, range: Range): void { - this.list?.revealElementRangeInCenter(cell, range); - } - - revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void { - this.list?.revealElementRangeInCenterIfOutsideViewport(cell, range); - } - - setCellSelection(cell: ICellViewModel, range: Range): void { - 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, true); - } - - //#endregion - - //#region Mouse Events - private readonly _onMouseUp: Emitter = this._register(new Emitter()); - public readonly onMouseUp: Event = this._onMouseUp.event; - - private readonly _onMouseDown: Emitter = this._register(new Emitter()); - public readonly onMouseDown: Event = this._onMouseDown.event; - - //#endregion - - //#region Cell operations - async layoutNotebookCell(cell: ICellViewModel, height: number): Promise { - const viewIndex = this.list!.getViewIndex(cell); - if (viewIndex === undefined) { - // the cell is hidden - return; - } - - let relayout = (cell: ICellViewModel, height: number) => { - this.list?.updateElementHeight2(cell, height); - }; - - let r: () => void; - DOM.scheduleAtNextAnimationFrame(() => { - relayout(cell, height); - r(); - }); - - return new Promise(resolve => { r = resolve; }); - } - - insertNotebookCell(cell: ICellViewModel | undefined, type: CellKind, direction: 'above' | 'below' = 'above', initialText: string = '', ui: boolean = false): CellViewModel | null { - if (!this.notebookViewModel!.metadata.editable) { - return null; - } - - const newLanguages = this.notebookViewModel!.languages; - const language = (type === CellKind.Code && newLanguages && newLanguages.length) ? newLanguages[0] : 'markdown'; - const index = cell ? this.notebookViewModel!.getCellIndex(cell) : 0; - const nextIndex = ui ? this.notebookViewModel!.getNextVisibleCellIndex(index) : index + 1; - const insertIndex = cell ? - (direction === 'above' ? index : nextIndex) : - index; - const newCell = this.notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, true); - return newCell; - } - - private isAtEOL(p: IPosition, lines: string[]) { - const line = lines[p.lineNumber - 1]; - return line.length + 1 === p.column; - } - - private pushIfAbsent(positions: IPosition[], p: IPosition) { - const last = positions.length > 0 ? positions[positions.length - 1] : undefined; - if (!last || last.lineNumber !== p.lineNumber || last.column !== p.column) { - positions.push(p); - } - } - - /** - * Add split point at the beginning and the end; - * Move end of line split points to the beginning of the next line; - * Avoid duplicate split points - */ - private splitPointsToBoundaries(splitPoints: IPosition[], lines: string[]): IPosition[] | null { - const boundaries: IPosition[] = []; - - // split points need to be sorted - splitPoints = splitPoints.sort((l, r) => { - const lineDiff = l.lineNumber - r.lineNumber; - const columnDiff = l.column - r.column; - return lineDiff !== 0 ? lineDiff : columnDiff; - }); - - // eat-up any split point at the beginning, i.e. we ignore the split point at the very beginning - this.pushIfAbsent(boundaries, new Position(1, 1)); - - for (let sp of splitPoints) { - if (this.isAtEOL(sp, lines) && sp.lineNumber < lines.length) { - sp = new Position(sp.lineNumber + 1, 1); - } - this.pushIfAbsent(boundaries, sp); - } - - // eat-up any split point at the beginning, i.e. we ignore the split point at the very end - this.pushIfAbsent(boundaries, new Position(lines.length, lines[lines.length - 1].length + 1)); - - // if we only have two then they describe the whole range and nothing needs to be split - return boundaries.length > 2 ? boundaries : null; - } - - private computeCellLinesContents(cell: IEditableCellViewModel, splitPoints: IPosition[]): string[] | null { - const lines = cell.getLinesContent(); - const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, lines); - if (!rangeBoundaries) { - return null; - } - const newLineModels: string[] = []; - for (let i = 1; i < rangeBoundaries.length; i++) { - const start = rangeBoundaries[i - 1]; - const end = rangeBoundaries[i]; - - newLineModels.push(cell.textModel.getValueInRange(new Range(start.lineNumber, start.column, end.lineNumber, end.column))); - } - - return newLineModels; - } - - async splitNotebookCell(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return null; - } - - let splitPoints = cell.getSelectionsStartPosition(); - if (splitPoints && splitPoints.length > 0) { - await cell.resolveTextModel(); - - if (!cell.hasModel()) { - return null; - } - - let newLinesContents = this.computeCellLinesContents(cell, splitPoints); - if (newLinesContents) { - - // update the contents of the first cell - cell.textModel.applyEdits([ - { range: cell.textModel.getFullModelRange(), text: newLinesContents[0] } - ], true); - - // create new cells based on the new text models - const language = cell.model.language; - const kind = cell.cellKind; - let insertIndex = this.notebookViewModel!.getCellIndex(cell) + 1; - const newCells = []; - for (let j = 1; j < newLinesContents.length; j++, insertIndex++) { - newCells.push(this.notebookViewModel!.createCell(insertIndex, newLinesContents[j], language, kind, true)); - } - return newCells; - } - } - - return null; - } - - async joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return null; - } - - if (constraint && cell.cellKind !== constraint) { - return null; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - if (index === 0 && direction === 'above') { - return null; - } - - if (index === this.notebookViewModel!.length - 1 && direction === 'below') { - return null; - } - - if (direction === 'above') { - const above = this.notebookViewModel!.viewCells[index - 1]; - if (constraint && above.cellKind !== constraint) { - return null; - } - - await above.resolveTextModel(); - if (!above.hasModel()) { - return null; - } - - const insertContent = cell.getText(); - const aboveCellLineCount = above.textModel.getLineCount(); - const aboveCellLastLineEndColumn = above.textModel.getLineLength(aboveCellLineCount); - above.textModel.applyEdits([ - { range: new Range(aboveCellLineCount, aboveCellLastLineEndColumn + 1, aboveCellLineCount, aboveCellLastLineEndColumn + 1), text: insertContent } - ]); - - await this.deleteNotebookCell(cell); - return above; - } else { - const below = this.notebookViewModel!.viewCells[index + 1]; - if (constraint && below.cellKind !== constraint) { - return null; - } - - await cell.resolveTextModel(); - if (!cell.hasModel()) { - return null; - } - - const insertContent = below.getText(); - - const cellLineCount = cell.textModel.getLineCount(); - const cellLastLineEndColumn = cell.textModel.getLineLength(cellLineCount); - cell.textModel.applyEdits([ - { range: new Range(cellLineCount, cellLastLineEndColumn + 1, cellLineCount, cellLastLineEndColumn + 1), text: insertContent } - ]); - - await this.deleteNotebookCell(below); - return cell; - } - } - - async deleteNotebookCell(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - this.notebookViewModel!.deleteCell(index, true); - return true; - } - - async moveCellDown(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - if (index === this.notebookViewModel!.length - 1) { - return false; - } - - const newIdx = index + 1; - return this.moveCellToIndex(index, newIdx); - } - - async moveCellUp(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - if (index === 0) { - return false; - } - - const newIdx = index - 1; - return this.moveCellToIndex(index, newIdx); - } - - async moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - if (cell === relativeToCell) { - return false; - } - - const originalIdx = this.notebookViewModel!.getCellIndex(cell); - const relativeToIndex = this.notebookViewModel!.getCellIndex(relativeToCell); - - let newIdx = direction === 'above' ? relativeToIndex : relativeToIndex + 1; - if (originalIdx < newIdx) { - newIdx--; - } - - return this.moveCellToIndex(originalIdx, newIdx); - } - - private async moveCellToIndex(index: number, newIdx: number): Promise { - if (index === newIdx) { - return false; - } - - if (!this.notebookViewModel!.moveCellToIdx(index, newIdx, true)) { - throw new Error('Notebook Editor move cell, index out of range'); - } - - let r: (val: boolean) => void; - DOM.scheduleAtNextAnimationFrame(() => { - this.list?.revealElementInView(this.notebookViewModel!.viewCells[newIdx]); - r(true); - }); - - return new Promise(resolve => { r = resolve; }); - } - - editNotebookCell(cell: CellViewModel): void { - if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).editable) { - return; - } - - cell.editState = CellEditState.Editing; - - this.renderedEditors.get(cell)?.focus(); - } - - saveNotebookCell(cell: ICellViewModel): void { - cell.editState = CellEditState.Preview; - } - - getActiveCell() { - let elements = this.list?.getFocusedElements(); - - if (elements && elements.length) { - return elements[0]; - } - - return undefined; - } - - cancelNotebookExecution(): void { - if (!this.notebookViewModel!.currentTokenSource) { - throw new Error('Notebook is not executing'); - } - - - this.notebookViewModel!.currentTokenSource.cancel(); - this.notebookViewModel!.currentTokenSource = undefined; - } - - async executeNotebook(): Promise { - if (!this.notebookViewModel!.metadata.runnable) { - return; - } - - // return this.progressService.showWhile(this._executeNotebook()); - return this._executeNotebook(); - } - - async _executeNotebook(): Promise { - if (this.notebookViewModel!.currentTokenSource) { - return; - } - - const tokenSource = new CancellationTokenSource(); - try { - this.editorExecutingNotebook!.set(true); - this.notebookViewModel!.currentTokenSource = tokenSource; - - for (let cell of this.notebookViewModel!.viewCells) { - if (cell.cellKind === CellKind.Code) { - await this._executeNotebookCell(cell, tokenSource); - } - } - } finally { - this.editorExecutingNotebook!.set(false); - this.notebookViewModel!.currentTokenSource = undefined; - tokenSource.dispose(); - } - } - - cancelNotebookCellExecution(cell: ICellViewModel): void { - if (!cell.currentTokenSource) { - throw new Error('Cell is not executing'); - } - - cell.currentTokenSource.cancel(); - cell.currentTokenSource = undefined; - } - - async executeNotebookCell(cell: ICellViewModel): Promise { - if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).runnable) { - return; - } - - const tokenSource = new CancellationTokenSource(); - try { - this._executeNotebookCell(cell, tokenSource); - } finally { - tokenSource.dispose(); - } - } - - private async _executeNotebookCell(cell: ICellViewModel, tokenSource: CancellationTokenSource): Promise { - try { - cell.currentTokenSource = tokenSource; - const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; - if (provider) { - const viewType = provider.id; - const notebookUri = CellUri.parse(cell.uri)?.notebook; - if (notebookUri) { - return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle, tokenSource.token); - } - } - } finally { - cell.currentTokenSource = undefined; - } - } - - focusNotebookCell(cell: ICellViewModel, focusEditor: boolean) { - if (focusEditor) { - this.selectElement(cell); - this.list?.focusView(); - - cell.editState = CellEditState.Editing; - cell.focusMode = CellFocusMode.Editor; - this.revealInCenterIfOutsideViewport(cell); - } else { - let itemDOM = this.list?.domElementOfElement(cell); - if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { - (document.activeElement as HTMLElement).blur(); - } - - cell.editState = CellEditState.Preview; - cell.focusMode = CellFocusMode.Container; - - this.selectElement(cell); - this.revealInCenterIfOutsideViewport(cell); - this.list?.focusView(); - } - } - - //#endregion - - //#region MISC - - getLayoutInfo(): NotebookLayoutInfo { - if (!this.list) { - throw new Error('Editor is not initalized successfully'); - } - - return { - width: this.dimension!.width, - height: this.dimension!.height, - fontInfo: this.fontInfo! - }; - } - - triggerScroll(event: IMouseWheelEvent) { - this.list?.triggerScrollFromMouseWheelEvent(event); - } - - createInset(cell: CodeCellViewModel, output: IOutput, shadowContent: string, offset: number) { - if (!this.webview) { - return; - } - - let preloads = this.notebookViewModel!.renderers; - - if (!this.webview!.insetMapping.has(output)) { - let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; - this.webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); - } else { - let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; - let scrollTop = this.list?.scrollTop || 0; - - this.webview!.updateViewScrollTop(-scrollTop, [{ cell: cell, output: output, cellTop: cellTop }]); - } - } - - removeInset(output: IOutput) { - if (!this.webview) { - return; - } - - this.webview!.removeInset(output); - } - - hideInset(output: IOutput) { - if (!this.webview) { - return; - } - - this.webview!.hideInset(output); - } - - getOutputRenderer(): OutputRenderer { - return this.outputRenderer; - } - - postMessage(message: any) { - this.webview?.webview.sendMessage(message); - } - - //#endregion - - //#region Editor Contributions - public getContribution(id: string): T { - return (this._contributions[id] || null); - } - //#endregion dispose() { - const keys = Object.keys(this._contributions); - for (let i = 0, len = keys.length; i < len; i++) { - const contributionId = keys[i]; - this._contributions[contributionId].dispose(); - } - + this._widget.dispose(); super.dispose(); } @@ -1230,111 +172,3 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } } -const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground'; - -export const focusedCellIndicator = registerColor('notebook.focusedCellIndicator', { - light: new Color(new RGBA(102, 175, 224)), - dark: new Color(new RGBA(12, 125, 157)), - hc: new Color(new RGBA(0, 73, 122)) -}, nls.localize('notebook.focusedCellIndicator', "The color of the focused notebook cell indicator.")); - -export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { - dark: new Color(new RGBA(255, 255, 255, 0.06)), - light: new Color(new RGBA(237, 239, 249)), - hc: null -} - , nls.localize('notebook.outputContainerBackgroundColor', "The Color of the notebook output container background.")); - -// TODO currently also used for toolbar border, if we keep all of this, pick a generic name -export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeperator', { - dark: Color.fromHex('#808080').transparent(0.35), - light: Color.fromHex('#808080').transparent(0.35), - hc: contrastBorder -}, nls.localize('cellToolbarSeperator', "The color of seperator in Cell bottom toolbar")); - - -registerThemingParticipant((theme, collector) => { - const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); - if (color) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .monaco-editor-background, - .monaco-workbench .part.editor > .content .notebook-editor .cell .margin-view-overlays, - .monaco-workbench .part.editor > .content .notebook-editor .cell .cell-statusbar-container { background: ${color}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-drag-image .cell-editor-container > div { background: ${color} !important; }`); - } - const link = theme.getColor(textLinkForeground); - if (link) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output a, - .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a { color: ${link};} `); - } - const activeLink = theme.getColor(textLinkActiveForeground); - if (activeLink) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output a:hover, - .monaco-workbench .part.editor > .content .notebook-editor .cell .output a:active { color: ${activeLink}; }`); - } - const shortcut = theme.getColor(textPreformatForeground); - if (shortcut) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor code, - .monaco-workbench .part.editor > .content .notebook-editor .shortcut { color: ${shortcut}; }`); - } - const border = theme.getColor(contrastBorder); - if (border) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-editor { border-color: ${border}; }`); - } - const quoteBackground = theme.getColor(textBlockQuoteBackground); - if (quoteBackground) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor blockquote { background: ${quoteBackground}; }`); - } - const quoteBorder = theme.getColor(textBlockQuoteBorder); - if (quoteBorder) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor blockquote { border-color: ${quoteBorder}; }`); - } - - const containerBackground = theme.getColor(notebookOutputContainerColor); - if (containerBackground) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { background-color: ${containerBackground}; }`); - } - - const editorBackgroundColor = theme.getColor(editorBackground); - if (editorBackgroundColor) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container { border-top: solid 1px ${editorBackgroundColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > .monaco-toolbar { background-color: ${editorBackgroundColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); - } - - const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); - if (cellToolbarSeperator) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container .seperator { background-color: ${cellToolbarSeperator} }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container .seperator-short { background-color: ${cellToolbarSeperator} }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > .monaco-toolbar { border: solid 1px ${cellToolbarSeperator}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:hover .notebook-cell-focus-indicator, - .monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator { border-color: ${cellToolbarSeperator}; }`); - } - - const focusedCellIndicatorColor = theme.getColor(focusedCellIndicator); - if (focusedCellIndicatorColor) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.focused .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .cell-insertion-indicator { background-color: ${focusedCellIndicatorColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-editor-focus .cell-editor-part:before { outline: solid 1px ${focusedCellIndicatorColor}; }`); - } - - // const widgetShadowColor = theme.getColor(widgetShadow); - // if (widgetShadowColor) { - // collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { - // box-shadow: 0 0 8px 4px ${widgetShadowColor} - // }`) - // } - - // Cell Margin - 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 .markdown-cell-row .cell .cell-editor-part { margin-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; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator { left: ${CELL_MARGIN + CELL_RUN_GUTTER}px; right: ${CELL_MARGIN}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-drag-image .cell-editor-container > div { padding: ${EDITOR_TOP_PADDING}px 16px ${EDITOR_BOTTOM_PADDING}px 16px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .notebook-cell-focus-indicator { left: ${CELL_MARGIN}px; }`); -}); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index 088143de4db..af58a89697b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -3,14 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor'; +import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions } from 'vs/workbench/common/editor'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, basename } from 'vs/base/common/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +let NOTEBOOK_EDITOR_INPUT_HANDLE = 0; export class NotebookEditorInput extends EditorInput { private static readonly _instances = new Map(); @@ -34,12 +36,26 @@ export class NotebookEditorInput extends EditorInput { static readonly ID: string = 'workbench.input.notebook'; private textModel: NotebookEditorModel | null = null; + private _group: GroupIdentifier | undefined; + + public get group(): GroupIdentifier | undefined { + return this._group; + } + + public updateGroup(group: GroupIdentifier): void { + this._group = group; + } + + readonly id: number = NOTEBOOK_EDITOR_INPUT_HANDLE++; constructor( public resource: URI, public name: string, public readonly viewType: string | undefined, @INotebookService private readonly notebookService: INotebookService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + // @IEditorService private readonly editorService: IEditorService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); } @@ -85,6 +101,48 @@ export class NotebookEditorInput extends EditorInput { return undefined; } + async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + if (!this.textModel) { + return undefined; + } + + const dialogPath = this.textModel.resource; + const target = await this.fileDialogService.pickFileToSave(dialogPath, options?.availableFileSystems); + if (!target) { + return undefined; // save cancelled + } + + if (!await this.textModel.saveAs(target)) { + return undefined; + } + + return this._move(group, target)?.editor; + } + + move(group: GroupIdentifier, target: URI): IMoveResult | undefined { + if (this.textModel) { + const contributedNotebookProviders = this.notebookService.getContributedNotebookProviders(target); + + if (contributedNotebookProviders.find(provider => provider.id === this.textModel!.viewType)) { + return this._move(group, target); + } + } + return undefined; + } + + _move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { + const editorInput = NotebookEditorInput.getOrCreate(this.instantiationService, newResource, basename(newResource), this.viewType); + return { editor: editorInput }; + } + + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + if (this.textModel) { + await this.textModel.revert(options); + } + + return; + } + async resolve(): Promise { if (!await this.notebookService.canResolve(this.viewType!)) { throw new Error(`Cannot open notebook of type '${this.viewType}'`); @@ -96,6 +154,10 @@ export class NotebookEditorInput extends EditorInput { this._onDidChangeDirty.fire(); })); + if (this.textModel.isDirty()) { + this._onDidChangeDirty.fire(); + } + return this.textModel; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts new file mode 100644 index 00000000000..bb4d12af72f --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -0,0 +1,1285 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getZoomLevel } from 'vs/base/browser/browser'; +import * as DOM from 'vs/base/browser/dom'; +import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Color, RGBA } from 'vs/base/common/color'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { combinedDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import 'vs/css!./media/notebook'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IEditor } from 'vs/editor/common/editorCommon'; +import { IReadonlyTextBuffer } from 'vs/editor/common/model'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor'; +import { CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_BOTTOM_PADDING, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, IEditableCellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; +import { CellDragAndDropController, CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellKind, CellUri, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; +import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { generateUuid } from 'vs/base/common/uuid'; +import { Memento, MementoObject } from 'vs/workbench/common/memento'; + +const $ = DOM.$; + +export class NotebookEditorOptions extends EditorOptions { + + readonly cellOptions?: IResourceEditorInput; + + constructor(options: Partial) { + super(); + this.overwrite(options); + this.cellOptions = options.cellOptions; + } + + with(options: Partial): NotebookEditorOptions { + return new NotebookEditorOptions({ ...this, ...options }); + } +} + + + +export class NotebookEditorWidget extends Disposable implements INotebookEditor { + static readonly ID: string = 'workbench.editor.notebook'; + private static readonly EDITOR_MEMENTOS = new Map>(); + private _rootElement!: HTMLElement; + private overlayContainer!: HTMLElement; + private body!: HTMLElement; + private webview: BackLayerWebView | null = null; + private webviewTransparentCover: HTMLElement | null = null; + private list: INotebookCellList | undefined; + private renderedEditors: Map = new Map(); + private eventDispatcher: NotebookEventDispatcher | undefined; + private notebookViewModel: NotebookViewModel | undefined; + private localStore: DisposableStore = this._register(new DisposableStore()); + private fontInfo: BareFontInfo | undefined; + private dimension: DOM.Dimension | null = null; + private editorFocus: IContextKey | null = null; + private editorEditable: IContextKey | null = null; + private editorRunnable: IContextKey | null = null; + private editorExecutingNotebook: IContextKey | null = null; + private outputRenderer: OutputRenderer; + protected readonly _contributions: { [key: string]: INotebookEditorContribution; }; + private scrollBeyondLastLine: boolean; + private readonly memento: Memento; + + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IStorageService storageService: IStorageService, + @INotebookService private notebookService: INotebookService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @ILayoutService private readonly _layoutService: ILayoutService + ) { + super(); + this.memento = new Memento(NotebookEditorWidget.ID, storageService); + + this.outputRenderer = new OutputRenderer(this, this.instantiationService); + this._contributions = {}; + this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); + + this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor.scrollBeyondLastLine')) { + this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); + if (this.dimension) { + this.layout(this.dimension); + } + } + }); + } + + private readonly _onDidChangeModel = new Emitter(); + readonly onDidChangeModel: Event = this._onDidChangeModel.event; + + + set viewModel(newModel: NotebookViewModel | undefined) { + this.notebookViewModel = newModel; + this._onDidChangeModel.fire(); + } + + get viewModel() { + return this.notebookViewModel; + } + + private readonly _onDidChangeActiveEditor = this._register(new Emitter()); + readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; + + get activeCodeEditor(): IEditor | undefined { + const [focused] = this.list!.getFocusedElements(); + return this.renderedEditors.get(focused); + } + + //#region Editor Core + + protected getEditorMemento(editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento { + const mementoKey = `${this.getId()}${key}`; + + let editorMemento = NotebookEditorWidget.EDITOR_MEMENTOS.get(mementoKey); + if (!editorMemento) { + editorMemento = new EditorMemento(this.getId(), key, this.getMemento(StorageScope.WORKSPACE), limit, editorGroupService); + NotebookEditorWidget.EDITOR_MEMENTOS.set(mementoKey, editorMemento); + } + + return editorMemento; + } + + protected getMemento(scope: StorageScope): MementoObject { + return this.memento.getMemento(scope); + } + + + getId(): string { + return NotebookEditorWidget.ID; + } + + + public get isNotebookEditor() { + return true; + } + + updateEditorFocus() { + // Note - focus going to the webview will fire 'blur', but the webview element will be + // a descendent of the notebook editor root. + this.editorFocus?.set(DOM.isAncestor(document.activeElement, this.overlayContainer)); + } + + createEditor(parent: HTMLElement): void { + this._rootElement = DOM.append(parent, $('.notebook-editor')); + + this.overlayContainer = document.createElement('div'); + const id = generateUuid(); + this.overlayContainer.id = `notebook-${id}`; + this.overlayContainer.className = 'notebookOverlay'; + DOM.addClass(this.overlayContainer, 'notebook-editor'); + this.overlayContainer.style.visibility = 'hidden'; + + this._layoutService.container.appendChild(this.overlayContainer); + this.createBody(this.overlayContainer); + this.generateFontInfo(); + this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService); + this.editorFocus.set(true); + this.editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.contextKeyService); + this.editorEditable.set(true); + this.editorRunnable = NOTEBOOK_EDITOR_RUNNABLE.bindTo(this.contextKeyService); + this.editorRunnable.set(true); + this.editorExecutingNotebook = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.contextKeyService); + + const contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); + + for (const desc of contributions) { + try { + const contribution = this.instantiationService.createInstance(desc.ctor, this); + this._contributions[desc.id] = contribution; + } catch (err) { + onUnexpectedError(err); + } + } + } + + private generateFontInfo(): void { + const editorOptions = this.configurationService.getValue('editor'); + this.fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + } + + private createBody(parent: HTMLElement): void { + this.body = document.createElement('div'); + DOM.addClass(this.body, 'cell-list-container'); + this.createCellList(); + DOM.append(parent, this.body); + } + + private createCellList(): void { + DOM.addClass(this.body, 'cell-list-container'); + + const dndController = this._register(new CellDragAndDropController(this, this.body)); + const renders = [ + this.instantiationService.createInstance(CodeCellRenderer, this, this.renderedEditors, dndController), + this.instantiationService.createInstance(MarkdownCellRenderer, this.contextKeyService, this, dndController, this.renderedEditors), + ]; + + this.list = this.instantiationService.createInstance( + NotebookCellList, + 'NotebookCellList', + this.body, + this.instantiationService.createInstance(NotebookCellListDelegate), + renders, + this.contextKeyService, + { + setRowLineHeight: false, + setRowHeight: false, + supportDynamicHeights: true, + horizontalScrolling: false, + keyboardSupport: false, + mouseSupport: true, + multipleSelectionSupport: false, + enableKeyboardNavigation: true, + additionalScrollHeight: 0, + transformOptimization: false, + styleController: (_suffix: string) => { return this.list!; }, + overrideStyles: { + listBackground: editorBackground, + listActiveSelectionBackground: editorBackground, + listActiveSelectionForeground: foreground, + listFocusAndSelectionBackground: editorBackground, + listFocusAndSelectionForeground: foreground, + listFocusBackground: editorBackground, + listFocusForeground: foreground, + listHoverForeground: foreground, + listHoverBackground: editorBackground, + listHoverOutline: focusBorder, + listFocusOutline: focusBorder, + listInactiveSelectionBackground: editorBackground, + listInactiveSelectionForeground: foreground, + listInactiveFocusBackground: editorBackground, + listInactiveFocusOutline: editorBackground, + }, + accessibilityProvider: { + getAriaLabel() { return null; }, + getWidgetAriaLabel() { + return nls.localize('notebookTreeAriaLabel', "Notebook"); + } + } + }, + ); + dndController.setList(this.list); + + this.webview = this.instantiationService.createInstance(BackLayerWebView, this); + this.webview.webview.onDidBlur(() => this.updateEditorFocus()); + this.webview.webview.onDidFocus(() => this.updateEditorFocus()); + this._register(this.webview.onMessage(message => { + if (this.viewModel) { + this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.viewModel.uri, message); + } + })); + this.list.rowsContainer.appendChild(this.webview.element); + + this._register(this.list); + this._register(combinedDisposable(...renders)); + + // transparent cover + this.webviewTransparentCover = DOM.append(this.list.rowsContainer, $('.webview-cover')); + this.webviewTransparentCover.style.display = 'none'; + + this._register(DOM.addStandardDisposableGenericMouseDownListner(this.overlayContainer, (e: StandardMouseEvent) => { + if (DOM.hasClass(e.target, 'slider') && this.webviewTransparentCover) { + this.webviewTransparentCover.style.display = 'block'; + } + })); + + this._register(DOM.addStandardDisposableGenericMouseUpListner(this.overlayContainer, () => { + if (this.webviewTransparentCover) { + // no matter when + this.webviewTransparentCover.style.display = 'none'; + } + })); + + this._register(this.list.onMouseDown(e => { + if (e.element) { + this._onMouseDown.fire({ event: e.browserEvent, target: e.element }); + } + })); + + this._register(this.list.onMouseUp(e => { + if (e.element) { + this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); + } + })); + + this._register(this.list.onDidChangeFocus(_e => this._onDidChangeActiveEditor.fire(this))); + } + + getShadowDomNode() { + return this._rootElement; + } + + getDomNode() { + return this.overlayContainer; + } + + onWillHide() { + this.editorFocus?.set(false); + this.overlayContainer.style.visibility = 'hidden'; + this.overlayContainer.style.display = 'none'; + } + + getInnerWebview(): Webview | undefined { + return this.webview?.webview; + } + + + focus() { + this.editorFocus?.set(true); + this.list?.domFocus(); + } + + async setModel(model: NotebookEditorModel, viewState: INotebookEditorViewState | undefined, options: EditorOptions | undefined): Promise { + if (this.notebookViewModel === undefined || !this.notebookViewModel.equal(model.notebook) || this.webview === null) { + this.detachModel(); + await this.attachModel(model, viewState); + } + + // reveal cell if editor options tell to do so + if (options instanceof NotebookEditorOptions && options.cellOptions) { + const cellOptions = options.cellOptions; + const cell = this.notebookViewModel!.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); + if (cell) { + this.selectElement(cell); + this.revealInCenterIfOutsideViewport(cell); + const editor = this.renderedEditors.get(cell)!; + if (editor) { + if (cellOptions.options?.selection) { + const { selection } = cellOptions.options; + editor.setSelection({ + ...selection, + endLineNumber: selection.endLineNumber || selection.startLineNumber, + endColumn: selection.endColumn || selection.startColumn + }); + } + if (!cellOptions.options?.preserveFocus) { + editor.focus(); + } + } + } + } + } + + private detachModel() { + this.localStore.clear(); + this.list?.detachViewModel(); + this.viewModel?.dispose(); + // avoid event + this.notebookViewModel = undefined; + this.webview?.clearInsets(); + this.webview?.clearPreloadsCache(); + this.list?.clear(); + } + + private updateForMetadata(): void { + this.editorEditable?.set(!!this.viewModel!.metadata?.editable); + this.editorRunnable?.set(!!this.viewModel!.metadata?.runnable); + DOM.toggleClass(this.overlayContainer, 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); + DOM.toggleClass(this.getDomNode(), 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); + } + + private async attachModel(model: NotebookEditorModel, viewState: INotebookEditorViewState | undefined) { + if (!this.webview) { + this.webview = this.instantiationService.createInstance(BackLayerWebView, this); + this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element); + } + + await this.webview.waitForInitialization(); + + this.eventDispatcher = new NotebookEventDispatcher(); + this.viewModel = this.instantiationService.createInstance(NotebookViewModel, model.viewType, model.notebook, this.eventDispatcher, this.getLayoutInfo()); + this.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + + this.updateForMetadata(); + this.localStore.add(this.eventDispatcher.onDidChangeMetadata(() => { + this.updateForMetadata(); + })); + + // restore view states, including contributions + + { + // restore view state + this.viewModel.restoreEditorViewState(viewState); + + // contribution state restore + + const contributionsState = viewState?.contributionsState || {}; + const keys = Object.keys(this._contributions); + for (let i = 0, len = keys.length; i < len; i++) { + const id = keys[i]; + const contribution = this._contributions[id]; + if (typeof contribution.restoreViewState === 'function') { + contribution.restoreViewState(contributionsState[id]); + } + } + } + + this.webview?.updateRendererPreloads(this.viewModel.renderers); + + this.localStore.add(this.list!.onWillScroll(e => { + this.webview!.updateViewScrollTop(-e.scrollTop, []); + this.webviewTransparentCover!.style.top = `${e.scrollTop}px`; + })); + + this.localStore.add(this.list!.onDidChangeContentHeight(() => { + DOM.scheduleAtNextAnimationFrame(() => { + const scrollTop = this.list?.scrollTop || 0; + const scrollHeight = this.list?.scrollHeight || 0; + this.webview!.element.style.height = `${scrollHeight}px`; + + if (this.webview?.insetMapping) { + let updateItems: { cell: CodeCellViewModel, output: IOutput, cellTop: number }[] = []; + let removedItems: IOutput[] = []; + this.webview?.insetMapping.forEach((value, key) => { + const cell = value.cell; + const viewIndex = this.list?.getViewIndex(cell); + + if (viewIndex === undefined) { + return; + } + + if (cell.outputs.indexOf(key) < 0) { + // output is already gone + removedItems.push(key); + } + + const cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; + if (this.webview!.shouldUpdateInset(cell, key, cellTop)) { + updateItems.push({ + cell: cell, + output: key, + cellTop: cellTop + }); + } + }); + + removedItems.forEach(output => this.webview?.removeInset(output)); + + if (updateItems.length) { + this.webview?.updateViewScrollTop(-scrollTop, updateItems); + } + } + }); + })); + + this.list!.attachViewModel(this.viewModel); + this.localStore.add(this.list!.onDidRemoveOutput(output => { + this.removeInset(output); + })); + this.localStore.add(this.list!.onDidHideOutput(output => { + this.hideInset(output); + })); + + this.list!.layout(); + + // restore list state at last, it must be after list layout + this.restoreListViewState(viewState); + } + + private restoreListViewState(viewState: INotebookEditorViewState | undefined): void { + if (viewState?.scrollPosition !== undefined) { + this.list!.scrollTop = viewState!.scrollPosition.top; + this.list!.scrollLeft = viewState!.scrollPosition.left; + } else { + this.list!.scrollTop = 0; + this.list!.scrollLeft = 0; + } + + const focusIdx = typeof viewState?.focus === 'number' ? viewState.focus : 0; + if (focusIdx < this.list!.length) { + this.list!.setFocus([focusIdx]); + this.list!.setSelection([focusIdx]); + } else if (this.list!.length > 0) { + this.list!.setFocus([0]); + } + + if (viewState?.editorFocused) { + this.list?.focusView(); + const cell = this.notebookViewModel?.viewCells[focusIdx]; + if (cell) { + cell.focusMode = CellFocusMode.Editor; + } + } + } + + getEditorViewState() { + const state = this.notebookViewModel!.getEditorViewState(); + if (this.list) { + state.scrollPosition = { left: this.list.scrollLeft, top: this.list.scrollTop }; + let cellHeights: { [key: number]: number } = {}; + for (let i = 0; i < this.viewModel!.length; i++) { + const elm = this.viewModel!.viewCells[i] as CellViewModel; + if (elm.cellKind === CellKind.Code) { + cellHeights[i] = elm.layoutInfo.totalHeight; + } else { + cellHeights[i] = 0; + } + } + + state.cellTotalHeights = cellHeights; + + const focus = this.list.getFocus()[0]; + if (typeof focus === 'number') { + const element = this.notebookViewModel!.viewCells[focus]; + const itemDOM = this.list?.domElementOfElement(element!); + let editorFocused = false; + if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { + editorFocused = true; + } + + state.editorFocused = editorFocused; + state.focus = focus; + } + } + + // Save contribution view states + const contributionsState: { [key: string]: any } = {}; + + const keys = Object.keys(this._contributions); + for (const id of keys) { + const contribution = this._contributions[id]; + if (typeof contribution.saveViewState === 'function') { + contributionsState[id] = contribution.saveViewState(); + } + } + + state.contributionsState = contributionsState; + return state; + } + + // private saveEditorViewState(input: NotebookEditorInput): void { + // if (this.group && this.notebookViewModel) { + // } + // } + + // private loadTextEditorViewState(): INotebookEditorViewState | undefined { + // return this.editorMemento.loadEditorState(this.group, input.resource); + // } + + layout(dimension: DOM.Dimension): void { + this.dimension = new DOM.Dimension(dimension.width, dimension.height); + DOM.toggleClass(this._rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600); + DOM.toggleClass(this._rootElement, 'narrow-width', dimension.width < 600); + DOM.size(this.body, dimension.width, dimension.height); + this.list?.updateOptions({ additionalScrollHeight: this.scrollBeyondLastLine ? dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP : 0 }); + this.list?.layout(dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, dimension.width); + + this.overlayContainer.style.visibility = 'visible'; + this.overlayContainer.style.display = 'block'; + const containerRect = this._rootElement.getBoundingClientRect(); + this.overlayContainer.style.position = 'absolute'; + this.overlayContainer.style.top = `${containerRect.top}px`; + this.overlayContainer.style.left = `${containerRect.left}px`; + this.overlayContainer.style.width = `${dimension ? dimension.width : containerRect.width}px`; + this.overlayContainer.style.height = `${dimension ? dimension.height : containerRect.height}px`; + + if (this.webviewTransparentCover) { + this.webviewTransparentCover.style.height = `${dimension.height}px`; + this.webviewTransparentCover.style.width = `${dimension.width}px`; + } + + this.eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + } + + // protected saveState(): void { + // if (this.input instanceof NotebookEditorInput) { + // this.saveEditorViewState(this.input); + // } + + // super.saveState(); + // } + + //#endregion + + //#region Editor Features + + selectElement(cell: ICellViewModel) { + this.list?.selectElement(cell); + // this.viewModel!.selectionHandles = [cell.handle]; + } + + revealInView(cell: ICellViewModel) { + this.list?.revealElementInView(cell); + } + + revealInCenterIfOutsideViewport(cell: ICellViewModel) { + this.list?.revealElementInCenterIfOutsideViewport(cell); + } + + revealInCenter(cell: ICellViewModel) { + this.list?.revealElementInCenter(cell); + } + + revealLineInView(cell: ICellViewModel, line: number): void { + this.list?.revealElementLineInView(cell, line); + } + + revealLineInCenter(cell: ICellViewModel, line: number) { + this.list?.revealElementLineInCenter(cell, line); + } + + revealLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number) { + this.list?.revealElementLineInCenterIfOutsideViewport(cell, line); + } + + revealRangeInView(cell: ICellViewModel, range: Range): void { + this.list?.revealElementRangeInView(cell, range); + } + + revealRangeInCenter(cell: ICellViewModel, range: Range): void { + this.list?.revealElementRangeInCenter(cell, range); + } + + revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void { + this.list?.revealElementRangeInCenterIfOutsideViewport(cell, range); + } + + setCellSelection(cell: ICellViewModel, range: Range): void { + 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, true); + } + + //#endregion + + //#region Mouse Events + private readonly _onMouseUp: Emitter = this._register(new Emitter()); + public readonly onMouseUp: Event = this._onMouseUp.event; + + private readonly _onMouseDown: Emitter = this._register(new Emitter()); + public readonly onMouseDown: Event = this._onMouseDown.event; + + //#endregion + + //#region Cell operations + async layoutNotebookCell(cell: ICellViewModel, height: number): Promise { + const viewIndex = this.list!.getViewIndex(cell); + if (viewIndex === undefined) { + // the cell is hidden + return; + } + + let relayout = (cell: ICellViewModel, height: number) => { + this.list?.updateElementHeight2(cell, height); + }; + + let r: () => void; + DOM.scheduleAtNextAnimationFrame(() => { + relayout(cell, height); + r(); + }); + + return new Promise(resolve => { r = resolve; }); + } + + insertNotebookCell(cell: ICellViewModel | undefined, type: CellKind, direction: 'above' | 'below' = 'above', initialText: string = '', ui: boolean = false): CellViewModel | null { + if (!this.notebookViewModel!.metadata.editable) { + return null; + } + + const newLanguages = this.notebookViewModel!.languages; + const language = (type === CellKind.Code && newLanguages && newLanguages.length) ? newLanguages[0] : 'markdown'; + const index = cell ? this.notebookViewModel!.getCellIndex(cell) : 0; + const nextIndex = ui ? this.notebookViewModel!.getNextVisibleCellIndex(index) : index + 1; + const insertIndex = cell ? + (direction === 'above' ? index : nextIndex) : + index; + const newCell = this.notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, true); + return newCell; + } + + private pushIfAbsent(positions: IPosition[], p: IPosition) { + const last = positions.length > 0 ? positions[positions.length - 1] : undefined; + if (!last || last.lineNumber !== p.lineNumber || last.column !== p.column) { + positions.push(p); + } + } + + /** + * Add split point at the beginning and the end; + * Move end of line split points to the beginning of the next line; + * Avoid duplicate split points + */ + private splitPointsToBoundaries(splitPoints: IPosition[], textBuffer: IReadonlyTextBuffer): IPosition[] | null { + const boundaries: IPosition[] = []; + const lineCnt = textBuffer.getLineCount(); + const getLineLen = (lineNumber: number) => { + return textBuffer.getLineLength(lineNumber); + }; + + // split points need to be sorted + splitPoints = splitPoints.sort((l, r) => { + const lineDiff = l.lineNumber - r.lineNumber; + const columnDiff = l.column - r.column; + return lineDiff !== 0 ? lineDiff : columnDiff; + }); + + // eat-up any split point at the beginning, i.e. we ignore the split point at the very beginning + this.pushIfAbsent(boundaries, new Position(1, 1)); + + for (let sp of splitPoints) { + if (getLineLen(sp.lineNumber) + 1 === sp.column && sp.lineNumber < lineCnt) { + sp = new Position(sp.lineNumber + 1, 1); + } + this.pushIfAbsent(boundaries, sp); + } + + // eat-up any split point at the beginning, i.e. we ignore the split point at the very end + this.pushIfAbsent(boundaries, new Position(lineCnt, getLineLen(lineCnt) + 1)); + + // if we only have two then they describe the whole range and nothing needs to be split + return boundaries.length > 2 ? boundaries : null; + } + + private computeCellLinesContents(cell: IEditableCellViewModel, splitPoints: IPosition[]): string[] | null { + const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, cell.textBuffer); + if (!rangeBoundaries) { + return null; + } + const newLineModels: string[] = []; + for (let i = 1; i < rangeBoundaries.length; i++) { + const start = rangeBoundaries[i - 1]; + const end = rangeBoundaries[i]; + + newLineModels.push(cell.textModel.getValueInRange(new Range(start.lineNumber, start.column, end.lineNumber, end.column))); + } + + return newLineModels; + } + + async splitNotebookCell(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return null; + } + + let splitPoints = cell.getSelectionsStartPosition(); + if (splitPoints && splitPoints.length > 0) { + await cell.resolveTextModel(); + + if (!cell.hasModel()) { + return null; + } + + let newLinesContents = this.computeCellLinesContents(cell, splitPoints); + if (newLinesContents) { + + // update the contents of the first cell + cell.textModel.applyEdits([ + { range: cell.textModel.getFullModelRange(), text: newLinesContents[0] } + ], true); + + // create new cells based on the new text models + const language = cell.model.language; + const kind = cell.cellKind; + let insertIndex = this.notebookViewModel!.getCellIndex(cell) + 1; + const newCells = []; + for (let j = 1; j < newLinesContents.length; j++, insertIndex++) { + newCells.push(this.notebookViewModel!.createCell(insertIndex, newLinesContents[j], language, kind, true)); + } + return newCells; + } + } + + return null; + } + + async joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return null; + } + + if (constraint && cell.cellKind !== constraint) { + return null; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + if (index === 0 && direction === 'above') { + return null; + } + + if (index === this.notebookViewModel!.length - 1 && direction === 'below') { + return null; + } + + if (direction === 'above') { + const above = this.notebookViewModel!.viewCells[index - 1]; + if (constraint && above.cellKind !== constraint) { + return null; + } + + await above.resolveTextModel(); + if (!above.hasModel()) { + return null; + } + + const insertContent = cell.getText(); + const aboveCellLineCount = above.textModel.getLineCount(); + const aboveCellLastLineEndColumn = above.textModel.getLineLength(aboveCellLineCount); + above.textModel.applyEdits([ + { range: new Range(aboveCellLineCount, aboveCellLastLineEndColumn + 1, aboveCellLineCount, aboveCellLastLineEndColumn + 1), text: insertContent } + ]); + + await this.deleteNotebookCell(cell); + return above; + } else { + const below = this.notebookViewModel!.viewCells[index + 1]; + if (constraint && below.cellKind !== constraint) { + return null; + } + + await cell.resolveTextModel(); + if (!cell.hasModel()) { + return null; + } + + const insertContent = below.getText(); + + const cellLineCount = cell.textModel.getLineCount(); + const cellLastLineEndColumn = cell.textModel.getLineLength(cellLineCount); + cell.textModel.applyEdits([ + { range: new Range(cellLineCount, cellLastLineEndColumn + 1, cellLineCount, cellLastLineEndColumn + 1), text: insertContent } + ]); + + await this.deleteNotebookCell(below); + return cell; + } + } + + async deleteNotebookCell(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + this.notebookViewModel!.deleteCell(index, true); + return true; + } + + async moveCellDown(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + if (index === this.notebookViewModel!.length - 1) { + return false; + } + + const newIdx = index + 1; + return this.moveCellToIndex(index, newIdx); + } + + async moveCellUp(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + if (index === 0) { + return false; + } + + const newIdx = index - 1; + return this.moveCellToIndex(index, newIdx); + } + + async moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + if (cell === relativeToCell) { + return false; + } + + const originalIdx = this.notebookViewModel!.getCellIndex(cell); + const relativeToIndex = this.notebookViewModel!.getCellIndex(relativeToCell); + + let newIdx = direction === 'above' ? relativeToIndex : relativeToIndex + 1; + if (originalIdx < newIdx) { + newIdx--; + } + + return this.moveCellToIndex(originalIdx, newIdx); + } + + private async moveCellToIndex(index: number, newIdx: number): Promise { + if (index === newIdx) { + return false; + } + + if (!this.notebookViewModel!.moveCellToIdx(index, newIdx, true)) { + throw new Error('Notebook Editor move cell, index out of range'); + } + + let r: (val: boolean) => void; + DOM.scheduleAtNextAnimationFrame(() => { + this.list?.revealElementInView(this.notebookViewModel!.viewCells[newIdx]); + r(true); + }); + + return new Promise(resolve => { r = resolve; }); + } + + editNotebookCell(cell: CellViewModel): void { + if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).editable) { + return; + } + + cell.editState = CellEditState.Editing; + + this.renderedEditors.get(cell)?.focus(); + } + + saveNotebookCell(cell: ICellViewModel): void { + cell.editState = CellEditState.Preview; + } + + getActiveCell() { + let elements = this.list?.getFocusedElements(); + + if (elements && elements.length) { + return elements[0]; + } + + return undefined; + } + + cancelNotebookExecution(): void { + if (!this.notebookViewModel!.currentTokenSource) { + throw new Error('Notebook is not executing'); + } + + + this.notebookViewModel!.currentTokenSource.cancel(); + this.notebookViewModel!.currentTokenSource = undefined; + } + + async executeNotebook(): Promise { + if (!this.notebookViewModel!.metadata.runnable) { + return; + } + + // return this.progressService.showWhile(this._executeNotebook()); + return this._executeNotebook(); + } + + async _executeNotebook(): Promise { + if (this.notebookViewModel!.currentTokenSource) { + return; + } + + const tokenSource = new CancellationTokenSource(); + try { + this.editorExecutingNotebook!.set(true); + this.notebookViewModel!.currentTokenSource = tokenSource; + + for (let cell of this.notebookViewModel!.viewCells) { + if (cell.cellKind === CellKind.Code) { + await this._executeNotebookCell(cell, tokenSource); + } + } + } finally { + this.editorExecutingNotebook!.set(false); + this.notebookViewModel!.currentTokenSource = undefined; + tokenSource.dispose(); + } + } + + cancelNotebookCellExecution(cell: ICellViewModel): void { + if (!cell.currentTokenSource) { + throw new Error('Cell is not executing'); + } + + cell.currentTokenSource.cancel(); + cell.currentTokenSource = undefined; + } + + async executeNotebookCell(cell: ICellViewModel): Promise { + if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).runnable) { + return; + } + + const tokenSource = new CancellationTokenSource(); + try { + this._executeNotebookCell(cell, tokenSource); + } finally { + tokenSource.dispose(); + } + } + + private async _executeNotebookCell(cell: ICellViewModel, tokenSource: CancellationTokenSource): Promise { + try { + cell.currentTokenSource = tokenSource; + const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; + if (provider) { + const viewType = provider.id; + const notebookUri = CellUri.parse(cell.uri)?.notebook; + if (notebookUri) { + return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle, tokenSource.token); + } + } + } finally { + cell.currentTokenSource = undefined; + } + } + + focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { + if (focusItem === 'editor') { + this.selectElement(cell); + this.list?.focusView(); + + cell.editState = CellEditState.Editing; + cell.focusMode = CellFocusMode.Editor; + this.revealInCenterIfOutsideViewport(cell); + } else if (focusItem === 'output') { + this.selectElement(cell); + this.list?.focusView(); + + if (!this.webview) { + return; + } + this.webview.focusOutput(cell.id); + + cell.editState = CellEditState.Preview; + cell.focusMode = CellFocusMode.Container; + this.revealInCenterIfOutsideViewport(cell); + } else { + let itemDOM = this.list?.domElementOfElement(cell); + if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { + (document.activeElement as HTMLElement).blur(); + } + + cell.editState = CellEditState.Preview; + cell.focusMode = CellFocusMode.Container; + + this.selectElement(cell); + this.revealInCenterIfOutsideViewport(cell); + this.list?.focusView(); + } + } + + //#endregion + + //#region MISC + + getLayoutInfo(): NotebookLayoutInfo { + if (!this.list) { + throw new Error('Editor is not initalized successfully'); + } + + return { + width: this.dimension!.width, + height: this.dimension!.height, + fontInfo: this.fontInfo! + }; + } + + triggerScroll(event: IMouseWheelEvent) { + this.list?.triggerScrollFromMouseWheelEvent(event); + } + + createInset(cell: CodeCellViewModel, output: IOutput, shadowContent: string, offset: number) { + if (!this.webview) { + return; + } + + let preloads = this.notebookViewModel!.renderers; + + if (!this.webview!.insetMapping.has(output)) { + let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; + this.webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); + } else { + let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; + let scrollTop = this.list?.scrollTop || 0; + + this.webview!.updateViewScrollTop(-scrollTop, [{ cell: cell, output: output, cellTop: cellTop }]); + } + } + + removeInset(output: IOutput) { + if (!this.webview) { + return; + } + + this.webview!.removeInset(output); + } + + hideInset(output: IOutput) { + if (!this.webview) { + return; + } + + this.webview!.hideInset(output); + } + + getOutputRenderer(): OutputRenderer { + return this.outputRenderer; + } + + postMessage(message: any) { + this.webview?.webview.sendMessage(message); + } + + //#endregion + + //#region Editor Contributions + public getContribution(id: string): T { + return (this._contributions[id] || null); + } + + //#endregion + + dispose() { + const keys = Object.keys(this._contributions); + for (let i = 0, len = keys.length; i < len; i++) { + const contributionId = keys[i]; + this._contributions[contributionId].dispose(); + } + + this.overlayContainer.remove(); + + // this._layoutService.container.removeChild(this.overlayContainer); + + super.dispose(); + } + + toJSON(): any { + return { + notebookHandle: this.viewModel?.handle + }; + } +} + +const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground'; + +export const focusedCellIndicator = registerColor('notebook.focusedCellIndicator', { + light: new Color(new RGBA(102, 175, 224)), + dark: new Color(new RGBA(12, 125, 157)), + hc: new Color(new RGBA(0, 73, 122)) +}, nls.localize('notebook.focusedCellIndicator', "The color of the focused notebook cell indicator.")); + +export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { + dark: new Color(new RGBA(255, 255, 255, 0.06)), + light: new Color(new RGBA(237, 239, 249)), + hc: null +} + , nls.localize('notebook.outputContainerBackgroundColor', "The Color of the notebook output container background.")); + +// TODO currently also used for toolbar border, if we keep all of this, pick a generic name +export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeperator', { + dark: Color.fromHex('#808080').transparent(0.35), + light: Color.fromHex('#808080').transparent(0.35), + hc: contrastBorder +}, nls.localize('cellToolbarSeperator', "The color of seperator in Cell bottom toolbar")); + + +registerThemingParticipant((theme, collector) => { + collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element { + padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px; + box-sizing: border-box; + }`); + + const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); + if (color) { + collector.addRule(`.notebookOverlay .cell .monaco-editor-background, + .notebookOverlay .cell .margin-view-overlays, + .notebookOverlay .cell .cell-statusbar-container { background: ${color}; }`); + collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { background: ${color} !important; }`); + } + const link = theme.getColor(textLinkForeground); + if (link) { + collector.addRule(`.notebookOverlay .output a, + .notebookOverlay .cell.markdown a { color: ${link};} `); + } + const activeLink = theme.getColor(textLinkActiveForeground); + if (activeLink) { + collector.addRule(`.notebookOverlay .output a:hover, + .notebookOverlay .cell .output a:active { color: ${activeLink}; }`); + } + const shortcut = theme.getColor(textPreformatForeground); + if (shortcut) { + collector.addRule(`.notebookOverlay code, + .notebookOverlay .shortcut { color: ${shortcut}; }`); + } + const border = theme.getColor(contrastBorder); + if (border) { + collector.addRule(`.notebookOverlay .monaco-editor { border-color: ${border}; }`); + } + const quoteBackground = theme.getColor(textBlockQuoteBackground); + if (quoteBackground) { + collector.addRule(`.notebookOverlay blockquote { background: ${quoteBackground}; }`); + } + const quoteBorder = theme.getColor(textBlockQuoteBorder); + if (quoteBorder) { + collector.addRule(`.notebookOverlay blockquote { border-color: ${quoteBorder}; }`); + } + + const containerBackground = theme.getColor(notebookOutputContainerColor); + if (containerBackground) { + collector.addRule(`.notebookOverlay .output { background-color: ${containerBackground}; }`); + } + + const editorBackgroundColor = theme.getColor(editorBackground); + if (editorBackgroundColor) { + collector.addRule(`.notebookOverlay .cell-statusbar-container { border-top: solid 1px ${editorBackgroundColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row > .monaco-toolbar { background-color: ${editorBackgroundColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); + } + + const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); + if (cellToolbarSeperator) { + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .seperator { background-color: ${cellToolbarSeperator} }`); + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .seperator-short { background-color: ${cellToolbarSeperator} }`); + collector.addRule(`.notebookOverlay .monaco-list-row > .monaco-toolbar { border: solid 1px ${cellToolbarSeperator}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row:hover .notebook-cell-focus-indicator, + .notebookOverlay .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator { border-color: ${cellToolbarSeperator}; }`); + } + + const focusedCellIndicatorColor = theme.getColor(focusedCellIndicator); + if (focusedCellIndicatorColor) { + collector.addRule(`.notebookOverlay .monaco-list-row.focused .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); + collector.addRule(`.notebookOverlay > .cell-list-container > .cell-list-insertion-indicator { background-color: ${focusedCellIndicatorColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row.cell-editor-focus .cell-editor-part:before { outline: solid 1px ${focusedCellIndicatorColor}; }`); + } + + // const widgetShadowColor = theme.getColor(widgetShadow); + // if (widgetShadowColor) { + // collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { + // box-shadow: 0 0 8px 4px ${widgetShadowColor} + // }`) + // } + + // Cell Margin + collector.addRule(`.notebookOverlay .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(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { padding-top: ${EDITOR_TOP_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); + collector.addRule(`.notebookOverlay .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(`.notebookOverlay .markdown-cell-row .cell .cell-editor-part { margin-left: ${CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay .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(`.notebookOverlay .cell .run-button-container { width: ${CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay > .cell-list-container > .cell-list-insertion-indicator { left: ${CELL_MARGIN + CELL_RUN_GUTTER}px; right: ${CELL_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { padding: ${EDITOR_TOP_PADDING}px 16px ${EDITOR_BOTTOM_PADDING}px 16px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator { left: ${CELL_MARGIN}px; }`); +}); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 8e8a3d0c741..ca530378bbc 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -11,7 +11,7 @@ import { notebookProviderExtensionPoint, notebookRendererExtensionPoint } from ' import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo, NotebookDocumentMetadata, CellEditType, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { Iterable } from 'vs/base/common/iterator'; @@ -202,13 +202,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu return; } - async resolveNotebook(viewType: string, uri: URI): Promise { + async createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise { const provider = this._notebookProviders.get(viewType); if (!provider) { return undefined; } - const notebookModel = await provider.controller.resolveNotebook(viewType, uri); + const notebookModel = await provider.controller.createNotebook(viewType, uri, true, false); if (!notebookModel) { return undefined; } @@ -219,6 +219,39 @@ export class NotebookService extends Disposable implements INotebookService, ICu notebookModel, (model) => this._onWillDispose(model), ); + this._models[modelId] = modelData; + + notebookModel.metadata = metadata; + notebookModel.languages = languages; + + notebookModel.applyEdit(notebookModel.versionId, [ + { + editType: CellEditType.Insert, + index: 0, + cells: cells + } + ]); + + return modelData.model; + } + + async resolveNotebook(viewType: string, uri: URI, forceReload: boolean): Promise { + const provider = this._notebookProviders.get(viewType); + if (!provider) { + return undefined; + } + + let notebookModel: NotebookTextModel | undefined; + + notebookModel = await provider.controller.createNotebook(viewType, uri, false, forceReload); + + // new notebook model created + const modelId = MODEL_ID(uri); + const modelData = new ModelData( + notebookModel!, + (model) => this._onWillDispose(model), + ); + this._models[modelId] = modelData; return modelData.model; } @@ -265,7 +298,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu let provider = this._notebookProviders.get(viewType); if (provider) { - provider.controller.destoryNotebookDocument(notebook); + provider.controller.removeNotebookDocument(notebook); } } @@ -291,6 +324,16 @@ export class NotebookService extends Disposable implements INotebookService, ICu return false; } + async saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.saveAs(resource, target, token); + } + + return false; + } + onDidReceiveMessage(viewType: string, uri: URI, message: any): void { let provider = this._notebookProviders.get(viewType); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 5f7766fd2e4..73fbe04220a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -57,7 +57,6 @@ export class NotebookCellList extends WorkbenchList implements ID ) { super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); - this._previousFocusedElements = this.getFocusedElements(); this._localDisposableStore.add(this.onDidChangeFocus((e) => { this._previousFocusedElements.forEach(element => { @@ -122,6 +121,20 @@ export class NotebookCellList extends WorkbenchList implements ID } + elementAt(position: number): ICellViewModel { + return this.element(this.view.indexAt(position)); + } + + elementHeight(element: ICellViewModel): number { + let index = this._getViewIndexUpperBound(element); + if (index === undefined || index < 0 || index >= this.length) { + this._getViewIndexUpperBound(element); + throw new ListError(this.listUser, `Invalid index ${index}`); + } + + return this.view.elementHeight(index); + } + protected createMouseController(_options: IListOptions): MouseController { return new NotebookMouseController(this); } @@ -157,7 +170,7 @@ export class NotebookCellList extends WorkbenchList implements ID for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell)) { + if (this._viewModel!.hasCell(cell.handle)) { hideOutputs.push(...cell?.model.outputs); } else { deletedOutputs.push(...cell?.model.outputs); @@ -177,7 +190,7 @@ export class NotebookCellList extends WorkbenchList implements ID for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell)) { + if (this._viewModel!.hasCell(cell.handle)) { hideOutputs.push(...cell?.model.outputs); } else { deletedOutputs.push(...cell?.model.outputs); @@ -299,7 +312,7 @@ export class NotebookCellList extends WorkbenchList implements ID for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell)) { + if (this._viewModel!.hasCell(cell.handle)) { hideOutputs.push(...cell?.model.outputs); } else { deletedOutputs.push(...cell?.model.outputs); @@ -316,6 +329,18 @@ export class NotebookCellList extends WorkbenchList implements ID 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); + + const selectionsLeft = []; + this._viewModel!.selectionHandles.forEach(handle => { + if (this._viewModel!.hasCell(handle)) { + selectionsLeft.push(handle); + } + }); + + if (!selectionsLeft.length && this._viewModel!.viewCells) { + // after splice, the selected cells are deleted + this._viewModel!.selectionHandles = [this._viewModel!.viewCells[0].handle]; + } } getViewIndex(cell: ICellViewModel) { 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 9cd53cc37ff..ca595859c72 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -4,22 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import * as path from 'vs/base/common/path'; +import { isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { CELL_MARGIN, CELL_RUN_GUTTER } from 'vs/workbench/contrib/notebook/browser/constants'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { CELL_MARGIN, CELL_RUN_GUTTER } from 'vs/workbench/contrib/notebook/browser/constants'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { isWeb } from 'vs/base/common/platform'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export interface IDimensionMessage { __vscode_notebook_message: boolean; @@ -54,10 +54,22 @@ export interface IScrollAckMessage { version: number; } +export interface IBlurOutputMessage { + __vscode_notebook_message: boolean; + type: 'focus-editor'; + id: string; + focusNext?: boolean; +} + export interface IClearMessage { type: 'clear'; } +export interface IFocusOutputMessage { + type: 'focus-output'; + id: string; +} + export interface ICreationRequestMessage { type: 'html'; content: string; @@ -65,6 +77,7 @@ export interface ICreationRequestMessage { outputId: string; top: number; left: number; + initiallyHidden?: boolean; } export interface IContentWidgetTopRequest { @@ -93,13 +106,28 @@ export interface IUpdatePreloadResourceMessage { resources: string[]; } -type IMessage = IDimensionMessage | IScrollAckMessage | IWheelMessage | IMouseEnterMessage | IMouseLeaveMessage; +interface ICachedInset { + outputId: string; + cell: CodeCellViewModel; + preloads: ReadonlySet; + cachedCreation: ICreationRequestMessage; +} + +function html(strings: TemplateStringsArray, ...values: any[]): string { + let str = ''; + strings.forEach((string, i) => { + str += string + (values[i] || ''); + }); + return str; +} + +type IMessage = IDimensionMessage | IScrollAckMessage | IWheelMessage | IMouseEnterMessage | IMouseLeaveMessage | IBlurOutputMessage; let version = 0; export class BackLayerWebView extends Disposable { element: HTMLElement; webview!: WebviewElement; - insetMapping: Map = new Map(); + insetMapping: Map = new Map(); hiddenInsetMapping: Set = new Set(); reversedInsetMapping: Map = new Map(); preloadsCache: Map = new Map(); @@ -109,7 +137,6 @@ export class BackLayerWebView extends Disposable { public readonly onMessage: Event = this._onMessage.event; private _initalized: Promise; - constructor( public notebookEditor: INotebookEditor, @IWebviewService readonly webviewService: IWebviewService, @@ -161,7 +188,7 @@ ${loaderJs} } generateContent(outputNodePadding: number, coreDependencies: string) { - return /* html */` + return html` @@ -277,6 +304,37 @@ ${loaderJs} }); }; + function focusFirstFocusableInCell(cellId) { + const cellOutputContainer = document.getElementById(cellId); + if (cellOutputContainer) { + const focusableElement = cellOutputContainer.querySelector('[tabindex="0"], [href], button, input, option, select, textarea'); + focusableElement && focusableElement.focus(); + } + } + + function createFocusSink(cellId, outputId, focusNext) { + const element = document.createElement('div'); + element.tabIndex = 0; + element.addEventListener('focus', () => { + vscode.postMessage({ + __vscode_notebook_message: true, + type: 'focus-editor', + id: outputId, + focusNext + }); + + setTimeout(() => { // Wait a tick to prevent the focus indicator blinking before webview blurs + // Move focus off the focus sink - single use + focusFirstFocusableInCell(cellId); + }, 50); + + console.log(cellId, outputId); + console.log(document.activeElement); + }); + + return element; + } + window.addEventListener('wheel', handleWheel); window.addEventListener('message', event => { @@ -288,10 +346,15 @@ ${loaderJs} let cellOutputContainer = document.getElementById(id); let outputId = event.data.outputId; if (!cellOutputContainer) { + const container = document.getElementById('container'); + + const upperWrapperElement = createFocusSink(id, outputId); + container.appendChild(upperWrapperElement); + let newElement = document.createElement('div'); newElement.id = id; - document.getElementById('container').appendChild(newElement); + container.appendChild(newElement); cellOutputContainer = newElement; cellOutputContainer.addEventListener('mouseenter', () => { @@ -310,6 +373,9 @@ ${loaderJs} data: { } }); }); + + const lowerWrapperElement = createFocusSink(id, outputId, true); + container.appendChild(lowerWrapperElement); } let outputNode = document.createElement('div'); @@ -336,6 +402,9 @@ ${loaderJs} height: outputNode.clientHeight } }); + + // don't hide until after this step so that the height is right + cellOutputContainer.style.display = event.data.initiallyHidden ? 'none' : 'block'; } break; case 'view-scroll': @@ -384,6 +453,11 @@ ${loaderJs} preloadsContainer.appendChild(scriptTag) } break; + case 'focus-output': + { + focusFirstFocusableInCell(id); + break; + } } }); }()); @@ -410,6 +484,14 @@ ${loaderJs} this.openerService.open(link, { fromUserGesture: true }); })); + this._register(this.webview.onDidReload(() => { + this.preloadsCache.clear(); + for (const [output, inset] of this.insetMapping.entries()) { + this.updateRendererPreloads(inset.preloads); + this.webview.sendMessage({ ...inset.cachedCreation, initiallyHidden: this.hiddenInsetMapping.has(output) }); + } + })); + this._register(this.webview.onMessage((data: IMessage) => { if (data.__vscode_notebook_message) { if (data.type === 'dimension') { @@ -445,6 +527,25 @@ ${loaderJs} preventDefault: () => { }, stopPropagation: () => { } }); + } else if (data.type === 'focus-editor') { + const info = this.resolveOutputId(data.id); + if (info) { + if (data.focusNext) { + const idx = this.notebookEditor.viewModel?.getCellIndex(info.cell); + if (typeof idx !== 'number') { + return; + } + + const newCell = this.notebookEditor.viewModel?.viewCells[idx + 1]; + if (!newCell) { + return; + } + + this.notebookEditor.focusNotebookCell(newCell, 'editor'); + } else { + this.notebookEditor.focusNotebookCell(info.cell, 'editor'); + } + } } return; } @@ -480,7 +581,7 @@ ${loaderJs} return true; } - if (outputOffset === outputCache.cacheOffset) { + if (outputOffset === outputCache.cachedCreation.top) { return false; } @@ -494,7 +595,7 @@ ${loaderJs} let outputIndex = item.cell.outputs.indexOf(item.output); let outputOffset = item.cellTop + item.cell.getOutputOffset(outputIndex); - outputCache.cacheOffset = outputOffset; + outputCache.cachedCreation.top = outputOffset; this.hiddenInsetMapping.delete(item.output); return { @@ -544,7 +645,7 @@ ${loaderJs} }; this.webview.sendMessage(message); - this.insetMapping.set(output, { outputId: outputId, cell: cell, cacheOffset: initialTop }); + this.insetMapping.set(output, { outputId: outputId, cell: cell, preloads, cachedCreation: message }); this.hiddenInsetMapping.delete(output); this.reversedInsetMapping.set(outputId, output); } @@ -589,7 +690,17 @@ ${loaderJs} this.reversedInsetMapping = new Map(); } - updateRendererPreloads(preloads: Set) { + focusOutput(cellId: string) { + this.webview.focus(); + setTimeout(() => { // Need this, or focus decoration is not shown. No clue. + this.webview.sendMessage({ + type: 'focus-output', + id: cellId + }); + }, 50); + } + + updateRendererPreloads(preloads: ReadonlySet) { let resources: string[] = []; let extensionLocations: URI[] = []; preloads.forEach(preload => { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 40fcca5f469..26b06d62155 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -13,6 +13,7 @@ import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/lis import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { ActionRunner, IAction } from 'vs/base/common/actions'; +import { Delayer } from 'vs/base/common/async'; import { renderCodicons } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; @@ -26,6 +27,7 @@ import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { EditorOption, EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer'; @@ -37,11 +39,12 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { BOTTOM_CELL_TOOLBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { CancelCellAction, ChangeCellLanguageAction, ExecuteCellAction, INotebookCellActionContext, InsertCodeCellAction, InsertMarkdownCellAction } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { BaseCellRenderTemplate, CellEditState, CellRunState, CodeCellRenderTemplate, ICellViewModel, INotebookEditor, MarkdownCellRenderTemplate, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BaseCellRenderTemplate, CellEditState, CellRunState, CodeCellRenderTemplate, ICellViewModel, INotebookCellList, INotebookEditor, MarkdownCellRenderTemplate, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; @@ -50,8 +53,6 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { CellKind, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; const $ = DOM.$; @@ -328,7 +329,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR constructor( contextKeyService: IContextKeyService, - notehookEditor: INotebookEditor, + notebookEditor: INotebookEditor, dndController: CellDragAndDropController, private renderedEditors: Map, @IInstantiationService instantiationService: IInstantiationService, @@ -337,7 +338,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, ) { - super(instantiationService, notehookEditor, contextMenuService, configurationService, keybindingService, notificationService, contextKeyService, 'markdown', dndController); + super(instantiationService, notebookEditor, contextMenuService, configurationService, keybindingService, notificationService, contextKeyService, 'markdown', dndController); } get templateId() { @@ -380,7 +381,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR languageStatusBarItem: statusBar.languageStatusBarItem, toJSON: () => { return {}; } }; - this.dndController.addListeners(templateData, () => this.getDragImage(templateData)); + this.dndController.registerDragHandle(templateData, () => this.getDragImage(templateData)); return templateData; } @@ -485,25 +486,98 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR } const DRAGGING_CLASS = 'cell-dragging'; -const DRAGOVER_TOP_CLASS = 'cell-dragover-top'; -const DRAGOVER_BOTTOM_CLASS = 'cell-dragover-bottom'; - const GLOBAL_DRAG_CLASS = 'global-drag-active'; type DragImageProvider = () => HTMLElement; +interface CellDragEvent { + browserEvent: DragEvent; + draggedOverCell: ICellViewModel; + cellTop: number; + cellHeight: number; + dragPosRatio: number; +} + export class CellDragAndDropController extends Disposable { // TODO@roblourens - should probably use dataTransfer here, but any dataTransfer set makes the editor think I am dropping a file, need // to figure out how to prevent that private currentDraggedCell: ICellViewModel | undefined; + private listInsertionIndicator: HTMLElement; + + private list!: INotebookCellList; + + private isScrolling = false; + private scrollingDelayer: Delayer; + constructor( - private readonly notebookEditor: INotebookEditor + private readonly notebookEditor: INotebookEditor, + insertionIndicatorContainer: HTMLElement ) { super(); + this.listInsertionIndicator = DOM.append(insertionIndicatorContainer, $('.cell-list-insertion-indicator')); + this._register(domEvent(document.body, DOM.EventType.DRAG_START, true)(this.onGlobalDragStart.bind(this))); this._register(domEvent(document.body, DOM.EventType.DRAG_END, true)(this.onGlobalDragEnd.bind(this))); + + const addCellDragListener = (eventType: string, handler: (e: CellDragEvent) => void) => { + this._register( + Event.map( + domEvent(notebookEditor.getDomNode(), eventType), + this.toCellDragEvent.bind(this)) + (handler)); + }; + + addCellDragListener(DOM.EventType.DRAG_OVER, event => { + event.browserEvent.preventDefault(); + this.onCellDragover(event); + }); + addCellDragListener(DOM.EventType.DROP, event => { + event.browserEvent.preventDefault(); + this.onCellDrop(event); + }); + addCellDragListener(DOM.EventType.DRAG_LEAVE, event => { + event.browserEvent.preventDefault(); + this.onCellDragLeave(event); + }); + + this.scrollingDelayer = new Delayer(200); + } + + setList(value: INotebookCellList) { + this.list = value; + + this.list.onWillScroll(() => { + this.setInsertIndicatorVisibility(false); + this.isScrolling = true; + this.scrollingDelayer.trigger(() => { + this.isScrolling = false; + }); + }); + } + + private setInsertIndicatorVisibility(visible: boolean) { + this.listInsertionIndicator.style.opacity = visible ? '1' : '0'; + } + + private toCellDragEvent(event: DragEvent): CellDragEvent { + const targetTop = this.notebookEditor.getDomNode().getBoundingClientRect().top; + const dragOffset = this.list.scrollTop + event.clientY - targetTop; + const draggedOverCell = this.list.elementAt(dragOffset); + const cellTop = this.list.getAbsoluteTopOfElement(draggedOverCell); + const cellHeight = this.list.elementHeight(draggedOverCell); + + const dragPosInElement = dragOffset - cellTop; + const dragPosRatio = dragPosInElement / cellHeight; + + return { + browserEvent: event, + draggedOverCell, + cellTop, + cellHeight, + dragPosRatio + }; } private onGlobalDragStart() { @@ -514,21 +588,72 @@ export class CellDragAndDropController extends Disposable { this.notebookEditor.getDomNode().classList.remove(GLOBAL_DRAG_CLASS); } - addListeners(templateData: BaseCellRenderTemplate, dragImageProvider: DragImageProvider): void { + private onCellDragover(event: CellDragEvent): void { + if (this.isScrolling || this.currentDraggedCell === event.draggedOverCell) { + this.setInsertIndicatorVisibility(false); + return; + } + + const dropDirection = this.getDropInsertDirection(event); + const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight; + const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop; + if (insertionIndicatorTop >= 0) { + this.listInsertionIndicator.style.top = `${insertionIndicatorAbsolutePos - this.list.scrollTop}px`; + this.setInsertIndicatorVisibility(true); + } else { + this.setInsertIndicatorVisibility(false); + } + } + + private getDropInsertDirection(event: CellDragEvent): 'above' | 'below' { + return event.dragPosRatio < 0.5 ? 'above' : 'below'; + } + + private onCellDrop(event: CellDragEvent): void { + const draggedCell = this.currentDraggedCell!; + this.dragCleanup(); + + const isCopy = (event.browserEvent.ctrlKey && !platform.isMacintosh) || (event.browserEvent.altKey && platform.isMacintosh); + + const dropDirection = this.getDropInsertDirection(event); + const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight; + const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop; + const editorHeight = this.notebookEditor.getDomNode().getBoundingClientRect().height; + if (insertionIndicatorTop < 0 || insertionIndicatorTop > editorHeight) { + // Ignore drop, insertion point is off-screen + return; + } + + if (isCopy) { + this.copyCell(draggedCell, event.draggedOverCell, dropDirection); + } else { + this.moveCell(draggedCell, event.draggedOverCell, dropDirection); + } + } + + private onCellDragLeave(event: CellDragEvent): void { + if (!event.browserEvent.relatedTarget || !DOM.isAncestor(event.browserEvent.relatedTarget as HTMLElement, this.notebookEditor.getDomNode())) { + this.setInsertIndicatorVisibility(false); + } + } + + private dragCleanup(): void { + if (this.currentDraggedCell) { + this.currentDraggedCell.dragging = false; + this.currentDraggedCell = undefined; + } + + this.setInsertIndicatorVisibility(false); + } + + registerDragHandle(templateData: BaseCellRenderTemplate, dragImageProvider: DragImageProvider): void { const container = templateData.container; const dragHandle = templateData.focusIndicator; - const dragCleanup = () => { - if (this.currentDraggedCell) { - this.currentDraggedCell.dragging = false; - this.currentDraggedCell = undefined; - } - }; - templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_END)(() => { // Note, templateData may have a different element rendered into it by now container.classList.remove(DRAGGING_CLASS); - dragCleanup(); + this.dragCleanup(); })); templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_START)(event => { @@ -546,77 +671,21 @@ export class CellDragAndDropController extends Disposable { container.classList.add(DRAGGING_CLASS); })); - - templateData.disposables.add(domEvent(container, DOM.EventType.DRAG_OVER)(event => { - event.preventDefault(); - - const location = this.getDropInsertDirection(templateData, event); - DOM.toggleClass(container, DRAGOVER_TOP_CLASS, location === 'above'); - DOM.toggleClass(container, DRAGOVER_BOTTOM_CLASS, location === 'below'); - })); - - templateData.disposables.add(domEvent(container, DOM.EventType.DROP)(event => { - event.preventDefault(); - - const draggedCell = this.currentDraggedCell!; - dragCleanup(); - - const isCopy = (event.ctrlKey && !platform.isMacintosh) || (event.altKey && platform.isMacintosh); - - const direction = this.getDropInsertDirection(templateData, event); - if (direction) { - const dropTarget = templateData.currentRenderedCell!; - if (isCopy) { - this.copyCell(draggedCell, dropTarget, direction); - } else { - this.moveCell(draggedCell, dropTarget, direction); - } - } - - container.classList.remove(DRAGOVER_TOP_CLASS, DRAGOVER_BOTTOM_CLASS); - })); - - templateData.disposables.add(domEvent(container, DOM.EventType.DRAG_LEAVE)(event => { - if (!event.relatedTarget || !DOM.isAncestor(event.relatedTarget as HTMLElement, container)) { - container.classList.remove(DRAGOVER_TOP_CLASS, DRAGOVER_BOTTOM_CLASS); - } - })); } private moveCell(draggedCell: ICellViewModel, ontoCell: ICellViewModel, direction: 'above' | 'below') { const editState = draggedCell.editState; this.notebookEditor.moveCell(draggedCell, ontoCell, direction); - this.notebookEditor.focusNotebookCell(draggedCell, editState === CellEditState.Editing); + this.notebookEditor.focusNotebookCell(draggedCell, editState === CellEditState.Editing ? 'editor' : 'container'); } private copyCell(draggedCell: ICellViewModel, ontoCell: ICellViewModel, direction: 'above' | 'below') { const editState = draggedCell.editState; const newCell = this.notebookEditor.insertNotebookCell(ontoCell, draggedCell.cellKind, direction, draggedCell.getText()); if (newCell) { - this.notebookEditor.focusNotebookCell(newCell, editState === CellEditState.Editing); + this.notebookEditor.focusNotebookCell(newCell, editState === CellEditState.Editing ? 'editor' : 'container'); } } - - private getDropInsertDirection(templateData: BaseCellRenderTemplate, event: DragEvent): 'above' | 'below' | undefined { - if (templateData.currentRenderedCell === this.currentDraggedCell) { - return; - } - - const dragOffset = this.getDragOffset(templateData.container, event); - if (dragOffset < 0.3) { - return 'above'; - } else if (dragOffset >= 0.5) { - return 'below'; - } else { - return; - } - } - - private getDragOffset(container: HTMLElement, event: DragEvent): number { - const containerRect = container.getBoundingClientRect(); - const dragoverContainerY = event.clientY - containerRect.top; - return dragoverContainerY / containerRect.height; - } } export class CellLanguageStatusBarItem extends Disposable { @@ -861,7 +930,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende toJSON: () => { return {}; } }; - this.dndController.addListeners(templateData, () => new CodeCellDragImageRenderer().getDragImage(templateData, templateData.editor, 'code')); + this.dndController.registerDragHandle(templateData, () => new CodeCellDragImageRenderer().getDragImage(templateData, templateData.editor, 'code')); return templateData; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 7be807d4ac7..8d276c0a999 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -250,7 +250,6 @@ export class StatefullMarkdownCell extends Disposable { bindEditorListeners(model: ITextModel, dimension?: IDimension) { this.localDisposables.add(model.onDidChangeContent(() => { // we don't need to update view cell text anymore as the textbuffer is shared - // this.viewCell.setText(model.getLinesContent()); this.viewCell.clearHTML(); let clientHeight = this.markdownContainer.clientHeight; this.markdownContainer.innerHTML = ''; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 8702fb0ea9b..e17d301efb3 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -205,25 +205,6 @@ export abstract class BaseCellViewModel extends Disposable { return this.model.getValue(); } - getLinesContent(): string[] { - if (this._textModel) { - return this._textModel.getLinesContent(); - } - - return this.model.textBuffer.getLinesContent(); - } - - // setLinesContent(value: string[]) { - // if (this._textModel) { - // // TODO @rebornix we should avoid creating a new string here - // return this._textModel.setValue(value.join('\n')); - // } else { - // const range = this.model.getFullModelRange(); - // this.model.textBuffer. - // this.model.source = value; - // } - // } - private saveViewState(): void { if (!this._textEditor) { return; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index e8016684b27..6db71153304 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -268,7 +268,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); diffs.reverse().forEach(diff => { - this._viewCells.splice(diff[0], diff[1], ...diff[2]); + const deletedCells = this._viewCells.splice(diff[0], diff[1], ...diff[2]); + + deletedCells.forEach(cell => { + this._handleToViewCellMapping.delete(cell.handle); + }); + diff[2].forEach(cell => { this._handleToViewCellMapping.set(cell.handle, cell); this._localStore.add(cell); @@ -456,8 +461,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return index + 1; } - hasCell(cell: ICellViewModel) { - return this._handleToViewCellMapping.has(cell.handle); + hasCell(handle: number) { + return this._handleToViewCellMapping.has(handle); } getVersionId() { @@ -586,7 +591,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._viewCells.splice(deleteIndex, 1); this._handleToViewCellMapping.delete(deleteCell.handle); - this._notebook.removeCell(deleteIndex); + this._notebook.removeCell(deleteIndex, 1); this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] }); } @@ -638,7 +643,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._viewCells.splice(index, 1); this._handleToViewCellMapping.delete(viewCell.handle); - this._notebook.removeCell(index); + this._notebook.removeCell(index, 1); let endSelections: number[] = []; if (this.selectionHandles.length) { @@ -701,7 +706,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return true; } - geteEditorViewState(): INotebookEditorViewState { + getEditorViewState(): INotebookEditorViewState { const editingCells: { [key: number]: boolean } = {}; this._viewCells.filter(cell => cell.editState === CellEditState.Editing).forEach(cell => editingCells[cell.model.handle] = true); const editorViewStates: { [key: number]: editorCommon.ICodeEditorViewState } = {}; diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 4c5ca6ecf6b..f768bc5d1ce 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -7,7 +7,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ITextSnapshot } from 'vs/editor/common/model'; function compareRangesUsingEnds(a: [number, number], b: [number, number]): number { if (a[1] === b[1]) { @@ -17,6 +18,49 @@ function compareRangesUsingEnds(a: [number, number], b: [number, number]): numbe return a[1] - b[1]; } +export class NotebookTextModelSnapshot implements ITextSnapshot { + // private readonly _pieces: Ce[] = []; + private _index: number = -1; + + constructor(private _model: NotebookTextModel) { + // for (let i = 0; i < this._model.cells.length; i++) { + // const cell = this._model.cells[i]; + // this._pieces.push(this._model.cells[i].textBuffer.createSnapshot(true)); + // } + } + + read(): string | null { + + if (this._index === -1) { + this._index++; + return `{ "metadata": ${JSON.stringify(this._model.metadata)}, "languages": ${JSON.stringify(this._model.languages)}, "cells": [`; + } + + if (this._index < this._model.cells.length) { + const cell = this._model.cells[this._index]; + + const data = { + source: cell.getValue(), + metadata: cell.metadata, + cellKind: cell.cellKind, + language: cell.language + }; + + const rawStr = JSON.stringify(data); + const isLastCell = this._index === this._model.cells.length - 1; + + this._index++; + return isLastCell ? rawStr : (rawStr + ','); + } else if (this._index === this._model.cells.length) { + this._index++; + return `]}`; + } else { + return null; + } + } + +} + export class NotebookTextModel extends Disposable implements INotebookTextModel { private static _cellhandlePool: number = 0; @@ -77,6 +121,18 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata); } + initialize(cells: ICellDto2[]) { + this.cells = []; + this._versionId = 0; + + const mainCells = cells.map(cell => { + const cellHandle = NotebookTextModel._cellhandlePool++; + const cellUri = CellUri.generate(this.uri, cellHandle); + return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata); + }); + this.insertNewCell(0, mainCells); + } + applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[]): boolean { if (modelVersionId !== this._versionId) { return false; @@ -127,7 +183,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this.insertNewCell(insertEdit.index, mainCells); break; case CellEditType.Delete: - this.removeCell(operations[i].index); + this.removeCell(operations[i].index, operations[i].end - operations[i].start); break; } } @@ -142,6 +198,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return true; } + createSnapshot(preserveBOM?: boolean): ITextSnapshot { + return new NotebookTextModelSnapshot(this); + } + private _increaseVersionId(): void { this._versionId = this._versionId + 1; } @@ -250,17 +310,19 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return; } - removeCell(index: number) { + removeCell(index: number, count: number) { this._isUntitled = false; - let cell = this.cells[index]; - this._cellListeners.get(cell.handle)?.dispose(); - this._cellListeners.delete(cell.handle); - this.cells.splice(index, 1); + for (let i = index; i < index + count; i++) { + let cell = this.cells[i]; + this._cellListeners.get(cell.handle)?.dispose(); + this._cellListeners.delete(cell.handle); + } + this.cells.splice(index, count); this._onDidChangeContent.fire(); this._increaseVersionId(); - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, 1, []]] }); + this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, count, []]] }); } moveCellToIdx(index: number, newIdx: number) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index b2a349dd607..90006f52f2a 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -273,7 +273,7 @@ export enum CellEditType { } export interface ICellDto2 { - source: string[]; + source: string | string[]; language: string; cellKind: CellKind; outputs: IOutput[]; @@ -300,6 +300,13 @@ export interface INotebookEditData { renderers: number[]; } +export interface NotebookDataDto { + readonly cells: ICellDto2[]; + readonly languages: string[]; + readonly metadata: NotebookDocumentMetadata; +} + + export namespace CellUri { export const scheme = 'vscode-notebook'; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index fd66b9a06b7..823c790efbb 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -15,6 +15,8 @@ import { URI } from 'vs/base/common/uri'; import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { basename } from 'vs/base/common/resources'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { DefaultEndOfLine, ITextBuffer, EndOfLinePreference } from 'vs/editor/common/model'; export interface INotebookEditorModelManager { models: NotebookEditorModel[]; @@ -24,6 +26,13 @@ export interface INotebookEditorModelManager { get(resource: URI): NotebookEditorModel | undefined; } +export interface INotebookRevertOptions { + /** + * Go to disk bypassing any cache of the model if any. + */ + forceReadFromDisk?: boolean; +} + export class NotebookEditorModel extends EditorModel implements IWorkingCopy, INotebookEditorModel { private _dirty = false; @@ -47,7 +56,8 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN public readonly resource: URI, public readonly viewType: string, @INotebookService private readonly notebookService: INotebookService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @IBackupFileService private readonly backupFileService: IBackupFileService ) { super(); this._register(this.workingCopyService.registerWorkingCopy(this)); @@ -56,28 +66,91 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN capabilities = 0; async backup(): Promise { - return {}; + return { content: this._notebook.createSnapshot(true) }; } async revert(options?: IRevertOptions | undefined): Promise { + if (options?.soft) { + await this.backupFileService.discardBackup(this.resource); + return; + } + + await this.load({ forceReadFromDisk: true }); + this._dirty = false; + this._onDidChangeDirty.fire(); return; } - async load(): Promise { - const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource); + async load(options?: INotebookRevertOptions): Promise { + if (options?.forceReadFromDisk) { + return this.loadFromProvider(true); + } + if (this.isResolved()) { + return this; + } + + const backup = await this.backupFileService.resolve(this.resource); + + if (this.isResolved()) { + return this; // Make sure meanwhile someone else did not succeed in loading + } + + if (backup) { + try { + return await this.loadFromBackup(backup.value.create(DefaultEndOfLine.LF)); + } catch (error) { + // this.logService.error('[text file model] load() from backup', error); // ignore error and continue to load as file below + } + } + + return this.loadFromProvider(false); + } + + private async loadFromBackup(content: ITextBuffer): Promise { + const fullRange = content.getRangeAt(0, content.getLength()); + const data = JSON.parse(content.getValueInRange(fullRange, EndOfLinePreference.LF)); + + const notebook = await this.notebookService.createNotebookFromBackup(this.viewType!, this.resource, data.metadata, data.languages, data.cells); this._notebook = notebook!; this._name = basename(this._notebook!.uri); this._register(this._notebook.onDidChangeContent(() => { - this._dirty = true; - this._onDidChangeDirty.fire(); + this.setDirty(true); + this._onDidChangeContent.fire(); + })); + + await this.backupFileService.discardBackup(this.resource); + this.setDirty(true); + + return this; + } + + private async loadFromProvider(forceReloadFromDisk: boolean) { + const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk); + this._notebook = notebook!; + + this._name = basename(this._notebook!.uri); + + this._register(this._notebook.onDidChangeContent(() => { + this.setDirty(true); this._onDidChangeContent.fire(); })); return this; } + isResolved(): boolean { + return !!this._notebook; + } + + setDirty(newState: boolean) { + if (this._dirty !== newState) { + this._dirty = newState; + this._onDidChangeDirty.fire(); + } + } + isDirty() { return this._dirty; } @@ -89,6 +162,14 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN this._onDidChangeDirty.fire(); return true; } + + async saveAs(targetResource: URI): Promise { + const tokenSource = new CancellationTokenSource(); + await this.notebookService.saveAs(this.notebook.viewType, this.notebook.uri, targetResource, tokenSource.token); + this._dirty = false; + this._onDidChangeDirty.fire(); + return true; + } } export class NotebookEditorModelManager extends Disposable implements INotebookEditorModelManager { diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index f846f30ffe8..6a808bafa99 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Event } from 'vs/base/common/event'; -import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo, NotebookDocumentMetadata, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; @@ -17,12 +17,13 @@ import { INotebookEditorModelManager } from 'vs/workbench/contrib/notebook/commo export const INotebookService = createDecorator('notebookService'); export interface IMainNotebookController { - resolveNotebook(viewType: string, uri: URI): Promise; + createNotebook(viewType: string, uri: URI, forBackup: boolean, forceReload: boolean): Promise; executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise; onDidReceiveMessage(uri: URI, message: any): void; executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise; - destoryNotebookDocument(notebook: INotebookTextModel): Promise; + removeNotebookDocument(notebook: INotebookTextModel): Promise; save(uri: URI, token: CancellationToken): Promise; + saveAs(uri: URI, target: URI, token: CancellationToken): Promise; } export interface INotebookService { @@ -35,7 +36,8 @@ export interface INotebookService { registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]): void; unregisterNotebookRenderer(handle: number): void; getRendererInfo(handle: number): INotebookRendererInfo | undefined; - resolveNotebook(viewType: string, uri: URI): Promise; + resolveNotebook(viewType: string, uri: URI, forceReload: boolean): Promise; + createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise; executeNotebook(viewType: string, uri: URI): Promise; executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise; @@ -45,6 +47,7 @@ export interface INotebookService { destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void; updateActiveNotebookDocument(viewType: string, resource: URI): void; save(viewType: string, resource: URI, token: CancellationToken): Promise; + saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise; onDidReceiveMessage(viewType: string, uri: URI, message: any): void; setToCopy(items: NotebookCellTextModel[]): void; getToCopy(): NotebookCellTextModel[] | undefined; diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 35ded8ed17d..1271f64e9ee 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -3,24 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; -import { CellKind, IOutput, CellUri, NotebookCellMetadata, INotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookViewModel, IModelDecorationsChangeAccessor, CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotebookEditor, NotebookLayoutInfo, ICellViewModel, ICellRange, INotebookEditorMouseEvent, INotebookEditorContribution } 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 { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; -import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { Emitter, Event } from 'vs/base/common/event'; import { EditorModel } from 'vs/workbench/common/editor'; +import { ICellRange, ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CellKind, CellUri, INotebookEditorModel, IOutput, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; +import { ICompositeCodeEditor, IEditor } from 'vs/editor/common/editorCommon'; export class TestCell extends NotebookCellTextModel { constructor( public viewType: string, @@ -42,6 +43,8 @@ export class TestNotebookEditor implements INotebookEditor { constructor( ) { } + onDidChangeActiveEditor: Event = new Emitter().event; + activeCodeEditor: IEditor | undefined; getDomNode(): HTMLElement { throw new Error('Method not implemented.'); } @@ -171,7 +174,7 @@ export class TestNotebookEditor implements INotebookEditor { saveNotebookCell(cell: CellViewModel): void { // throw new Error('Method not implemented.'); } - focusNotebookCell(cell: CellViewModel, focusEditor: boolean): void { + focusNotebookCell(cell: CellViewModel, focusItem: 'editor' | 'container' | 'output'): void { // throw new Error('Method not implemented.'); } getActiveCell(): CellViewModel | undefined { diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index f20dcdfbcbe..8b7b0145b91 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -401,7 +401,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditorP } private createRecordingBadge(container: HTMLElement): HTMLElement { - const recordingBadge = DOM.append(container, DOM.$('.recording-badge.monaco-count-badge.disabled')); + const recordingBadge = DOM.append(container, DOM.$('.recording-badge.monaco-count-badge.long.disabled')); recordingBadge.textContent = localize('recording', "Recording Keys"); this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder, badgeForeground }, colors => { const background = colors.badgeBackground ? colors.badgeBackground.toString() : ''; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 45ab2bfe9a7..d0f5ac5841e 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -32,7 +32,7 @@ import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticip import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { PANEL_ACTIVE_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND } from 'vs/workbench/common/theme'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; +import { ISettingsGroup, IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { isEqual } from 'vs/base/common/resources'; import { registerIcon, Codicon } from 'vs/base/common/codicons'; @@ -310,7 +310,8 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { constructor( action: IAction, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IContextMenuService private readonly contextMenuService: IContextMenuService + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IPreferencesService private readonly preferencesService: IPreferencesService, ) { super(null, action); const workspace = this.contextService.getWorkspace(); @@ -400,14 +401,14 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { } } - private update(): void { + private async update(): Promise { let total = 0; this._folderSettingCounts.forEach(n => total += n); const workspace = this.contextService.getWorkspace(); if (this._folder) { this.labelElement.textContent = this._folder.name; - this.anchorElement.title = this._folder.name; + this.anchorElement.title = (await this.preferencesService.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, this._folder.uri))?.fsPath || ''; const detailsText = this.labelWithCount(this._action.label, total); this.detailsElement.textContent = detailsText; DOM.toggleClass(this.dropDownElement, 'hide', workspace.folders.length === 1 || !this._action.checked); @@ -418,6 +419,7 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { this.anchorElement.title = this._action.label; DOM.removeClass(this.dropDownElement, 'hide'); } + DOM.toggleClass(this.anchorElement, 'checked', this._action.checked); DOM.toggleClass(this.container, 'disabled', !this._action.enabled); } @@ -487,7 +489,8 @@ export class SettingsTargetsWidget extends Widget { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @ILabelService private readonly labelService: ILabelService + @ILabelService private readonly labelService: ILabelService, + @IPreferencesService private readonly preferencesService: IPreferencesService, ) { super(); this.options = options || {}; @@ -506,17 +509,21 @@ export class SettingsTargetsWidget extends Widget { })); this.userLocalSettings = new Action('userSettings', localize('userSettings', "User"), '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_LOCAL)); - this.userLocalSettings.tooltip = this.userLocalSettings.label; + this.preferencesService.getEditableSettingsURI(ConfigurationTarget.USER_LOCAL).then(uri => { + // Don't wait to create UI on resolving remote + this.userLocalSettings.tooltip = uri?.fsPath || ''; + }); const remoteAuthority = this.environmentService.configuration.remoteAuthority; const hostLabel = remoteAuthority && this.labelService.getHostLabel(REMOTE_HOST_SCHEME, remoteAuthority); const remoteSettingsLabel = localize('userSettingsRemote', "Remote") + (hostLabel ? ` [${hostLabel}]` : ''); this.userRemoteSettings = new Action('userSettingsRemote', remoteSettingsLabel, '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_REMOTE)); - this.userRemoteSettings.tooltip = this.userRemoteSettings.label; + this.preferencesService.getEditableSettingsURI(ConfigurationTarget.USER_REMOTE).then(uri => { + this.userRemoteSettings.tooltip = uri?.fsPath || ''; + }); this.workspaceSettings = new Action('workspaceSettings', localize('workspaceSettings', "Workspace"), '.settings-tab', false, () => this.updateTarget(ConfigurationTarget.WORKSPACE)); - this.workspaceSettings.tooltip = this.workspaceSettings.label; const folderSettingsAction = new Action('folderSettings', localize('folderSettings', "Folder"), '.settings-tab', false, (folder: IWorkspaceFolder | null) => this.updateTarget(folder ? folder.uri : ConfigurationTarget.USER_LOCAL)); @@ -586,13 +593,14 @@ export class SettingsTargetsWidget extends Widget { return Promise.resolve(undefined); } - private update(): void { + private async update(): Promise { DOM.toggleClass(this.settingsSwitcherBar.domNode, 'empty-workbench', this.contextService.getWorkbenchState() === WorkbenchState.EMPTY); this.userRemoteSettings.enabled = !!(this.options.enableRemoteSettings && this.environmentService.configuration.remoteAuthority); this.workspaceSettings.enabled = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY; this.folderSettings.getAction().enabled = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && this.contextService.getWorkspace().folders.length > 0; - } + this.workspaceSettings.tooltip = (await this.preferencesService.getEditableSettingsURI(ConfigurationTarget.WORKSPACE))?.fsPath || ''; + } } export interface SearchOptions extends IInputOptions { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 9a7443f2aaf..f7fbfb25ba0 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -442,7 +442,7 @@ export class SettingsEditor2 extends BaseEditor { inputBorder: settingsTextInputBorder })); - this.countElement = DOM.append(searchContainer, DOM.$('.settings-count-widget.monaco-count-badge')); + this.countElement = DOM.append(searchContainer, DOM.$('.settings-count-widget.monaco-count-badge.long')); this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder, badgeForeground }, colors => { const background = colors.badgeBackground ? colors.badgeBackground.toString() : ''; const border = colors.contrastBorder ? colors.contrastBorder.toString() : ''; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 21738f1e575..495a328ae6f 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -740,7 +740,7 @@ export class SettingComplexRenderer extends AbstractSettingRenderer implements I } private renderValidations(dataElement: SettingsTreeSettingElement, template: ISettingComplexItemTemplate) { - const errMsg = getInvalidTypeError(dataElement.value, dataElement.setting.type); + const errMsg = dataElement.isConfigured && getInvalidTypeError(dataElement.value, dataElement.setting.type); if (errMsg) { DOM.addClass(template.containerElement, 'invalid-input'); template.validationErrorMessageElement.innerText = errMsg; @@ -1486,7 +1486,7 @@ class SettingsTreeDelegate extends CachedListVirtualDelegate { + await this.commandService.executeCommand('openInTerminal', repository.provider.rootUri); + })); + } + if (secondary.length === 0) { return; } diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts index 2b1b9a3d03a..21b166c0ac4 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts @@ -638,6 +638,7 @@ export class ToggleViewModeAction extends Action { } export class RepositoryPane extends ViewPane { + private readonly defaultInputFontFamily = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif'; private cachedHeight: number | undefined = undefined; private cachedWidth: number | undefined = undefined; @@ -766,13 +767,12 @@ export class RepositoryPane extends ViewPane { cursorWidth: 1, fontSize: 13, lineHeight: 20, - fontFamily: ' -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif', + fontFamily: this.getInputEditorFontFamily(), wrappingStrategy: 'advanced', wrappingIndent: 'none', padding: { top: 3, bottom: 3 }, quickSuggestions: false }; - const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { isSimpleWidget: true, contributions: EditorExtensionsRegistry.getSomeEditorContributions([ @@ -796,6 +796,9 @@ export class RepositoryPane extends ViewPane { this._register(this.inputEditor.onDidFocusEditorText(() => addClass(editorContainer, 'synthetic-focus'))); this._register(this.inputEditor.onDidBlurEditorText(() => removeClass(editorContainer, 'synthetic-focus'))); + const onInputFontFamilyChanged = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.inputFontFamily')); + this._register(onInputFontFamilyChanged(() => this.inputEditor.updateOptions({ fontFamily: this.getInputEditorFontFamily() }))); + let query: string | undefined; if (this.repository.provider.rootUri) { @@ -1114,6 +1117,20 @@ export class RepositoryPane extends ViewPane { this.layoutBody(this.cachedHeight); } } + + private getInputEditorFontFamily(): string { + const inputFontFamily = this.configurationService.getValue('scm.inputFontFamily'); + + if (inputFontFamily.toLowerCase() === 'editor') { + return this.configurationService.getValue('editor.fontFamily'); + } + + if (inputFontFamily.length !== 0 && inputFontFamily.toLowerCase() !== 'default') { + return inputFontFamily; + } + + return this.defaultInputFontFamily; + } } export class RepositoryViewDescriptor implements IViewDescriptor { diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index ccbae2dad6c..6020e1c282d 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -150,6 +150,11 @@ Registry.as(ConfigurationExtensions.Configuration).regis description: localize('autoReveal', "Controls whether the SCM view should automatically reveal and select files when opening them."), default: true }, + 'scm.inputFontFamily': { + type: 'string', + markdownDescription: localize('inputFontFamily', "Controls the font for the input message. Use `default` for the workbench user interface font family, `editor` for the `#editor.fontFamily#`'s value, or a custom font family."), + default: 'default' + } } }); diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index 27ad06b314c..8c8cedc0472 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -152,9 +152,7 @@ } .search-view .message { - padding-left: 22px; - padding-right: 22px; - padding-top: 0px; + padding: 0 22px 8px; } .search-view .message p:first-child { diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index b3136e0a0ab..6f28920018f 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -826,6 +826,11 @@ configurationRegistry.registerConfiguration({ ], markdownDescription: nls.localize('search.searchEditor.doubleClickBehaviour', "Configure effect of double clicking a result in a search editor.") }, + 'search.searchEditor.reusePriorSearchConfiguration': { + type: 'boolean', + default: false, + markdownDescription: nls.localize('search.searchEditor.reusePriorSearchConfiguration', "When enabled, new Search Editors will reuse the includes, excludes, and flags of the previously opened Search Editor") + }, 'search.sortOrder': { 'type': 'string', 'enum': [SearchSortOrder.Default, SearchSortOrder.FileNames, SearchSortOrder.Type, SearchSortOrder.Modified, SearchSortOrder.CountDescending, SearchSortOrder.CountAscending], diff --git a/src/vs/workbench/contrib/searchEditor/browser/constants.ts b/src/vs/workbench/contrib/searchEditor/browser/constants.ts index 0040e022187..cc24ea515f6 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/constants.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/constants.ts @@ -12,3 +12,5 @@ export const SearchEditorScheme = 'search-editor'; export const SearchEditorFindMatchClass = 'seaarchEditorFindMatch'; export const SearchEditorID = 'workbench.editor.searchEditor'; + +export const OpenNewEditorCommandId = 'search.action.openNewEditor'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 223ec269470..d3c445104db 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -8,9 +8,10 @@ import * as objects from 'vs/base/common/objects'; import { endsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { Range } from 'vs/editor/common/core/range'; import { ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel'; import { localize } from 'vs/nls'; -import { MenuId, SyncActionDescriptor, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -20,20 +21,18 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; -import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry, ActiveEditorContext } from 'vs/workbench/common/editor'; +import { ActiveEditorContext, Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; +import { IViewsService } from 'vs/workbench/common/views'; +import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions'; +import { searchRefreshIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import * as SearchConstants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { modifySearchEditorContextLinesCommand, OpenSearchEditorAction, selectAllSearchEditorMatchesCommand, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, createEditorFromSearchResult, openNewSearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; -import { getOrMakeSearchEditorInput, SearchEditorInput, SearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { createEditorFromSearchResult, modifySearchEditorContextLinesCommand, openNewSearchEditor, selectAllSearchEditorMatchesCommand, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; +import { getOrMakeSearchEditorInput, SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { parseSavedSearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; -import { Range } from 'vs/editor/common/core/range'; -import { searchRefreshIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; -import { IViewsService } from 'vs/workbench/common/views'; -import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; const OpenInEditorCommandId = 'search.action.openInEditor'; @@ -210,13 +209,57 @@ CommandsRegistry.registerCommand( //#endregion //#region Actions -const registry = Registry.as(ActionExtensions.WorkbenchActions); const category = localize('search', "Search Editor"); -// TODO: Not an action2 becuase used in view pane container action bar, which uses actions -registry.registerWorkbenchAction( - SyncActionDescriptor.from(OpenSearchEditorAction), - 'Search Editor: Open New Search Editor', category); +const openArgDescription = { + description: 'Open a new search editor. Arguments passed can include variables like ${relativeFileDirname}.', + args: [{ + name: 'Open new Search Editor args', + schema: { + properties: { + query: { type: 'string' }, + includes: { type: 'string' }, + excludes: { type: 'string' }, + contextLines: { type: 'number' }, + wholeWord: { type: 'boolean' }, + caseSensitive: { type: 'boolean' }, + regexp: { type: 'boolean' }, + useIgnores: { type: 'boolean' }, + showIncludesExcludes: { type: 'boolean' }, + } + } + }] +} as const; + +registerAction2(class extends Action2 { + constructor() { + super({ + id: SearchEditorConstants.OpenNewEditorCommandId, + title: localize('search.openNewSearchEditor', "Open new Search Editor"), + category, + f1: true, + description: openArgDescription + }); + } + async run(accessor: ServicesAccessor, args: Partial) { + await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, args); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: OpenNewEditorToSideCommandId, + title: localize('search.openNewEditorToSide', "Open new Search Editor to the Side"), + category, + f1: true, + description: openArgDescription + }); + } + async run(accessor: ServicesAccessor, args: Partial) { + await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, args, true); + } +}); registerAction2(class extends Action2 { constructor() { @@ -242,21 +285,6 @@ registerAction2(class extends Action2 { } }); -registerAction2(class extends Action2 { - constructor() { - super({ - id: OpenNewEditorToSideCommandId, - title: localize('search.openNewEditorToSide', "Open New Search Editor to Side"), - category, - f1: true, - }); - } - async run(accessor: ServicesAccessor) { - const instantiationService = accessor.get(IInstantiationService); - await instantiationService.invokeFunction(openNewSearchEditor, true); - } -}); - registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index acf2e2672ac..19acdf21da1 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -14,13 +14,17 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; +import { getOrMakeSearchEditorInput, SearchEditorInput, SearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; import { searchNewEditorIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; - -const OpenNewEditorCommandId = 'search.action.openNewEditor'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; +import { Schemas } from 'vs/base/common/network'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { OpenNewEditorCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants'; export const toggleSearchEditorCaseSensitiveCommand = (accessor: ServicesAccessor) => { const editorService = accessor.get(IEditorService); @@ -95,12 +99,23 @@ export class OpenSearchEditorAction extends Action { } export const openNewSearchEditor = - async (accessor: ServicesAccessor, toSide = false) => { + async (accessor: ServicesAccessor, args: Partial = {}, toSide = false) => { const editorService = accessor.get(IEditorService); const telemetryService = accessor.get(ITelemetryService); const instantiationService = accessor.get(IInstantiationService); const configurationService = accessor.get(IConfigurationService); + const configurationResolverService = accessor.get(IConfigurationResolverService); + const workspaceContextService = accessor.get(IWorkspaceContextService); + const historyService = accessor.get(IHistoryService); + const activeWorkspaceRootUri = historyService.getLastActiveWorkspaceRoot(Schemas.file); + const lastActiveWorkspaceRoot = activeWorkspaceRootUri ? withNullAsUndefined(workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; + + const resolvedArgs: Record = {}; + Object.entries(args).forEach(([name, value]) => { + resolvedArgs[name as any] = (typeof value === 'string') ? configurationResolverService.resolve(lastActiveWorkspaceRoot, value) : value; + }); + const activeEditorControl = editorService.activeTextEditorControl; let activeModel: ICodeEditor | undefined; let selected = ''; @@ -125,7 +140,7 @@ export const openNewSearchEditor = telemetryService.publicLog2('searchEditor/openNewSearchEditor'); - const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: { query: selected }, text: '' }); + const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: { query: selected, ...resolvedArgs }, text: '' }); const editor = await editorService.openEditor(input, { pinned: true }, toSide ? SIDE_GROUP : ACTIVE_GROUP) as SearchEditor; if (selected && configurationService.getValue('search').searchOnType) { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index 1faf608f211..b2d8dc6834c 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -36,7 +36,7 @@ import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapab export type SearchConfiguration = { query: string, includes: string, - excludes: string + excludes: string, contextLines: number, wholeWord: boolean, caseSensitive: boolean, @@ -304,7 +304,7 @@ export const getOrMakeSearchEditorInput = ( const storageService = accessor.get(IStorageService); const configurationService = accessor.get(IConfigurationService); - const reuseOldSettings = configurationService.getValue('search').searchEditor?.experimental?.reusePriorSearchConfiguration; + const reuseOldSettings = configurationService.getValue('search').searchEditor?.reusePriorSearchConfiguration; const priorConfig: SearchConfiguration = reuseOldSettings ? new Memento(SearchEditorInput.ID, storageService).getMemento(StorageScope.WORKSPACE).searchConfig : {}; const defaultConfig = defaultSearchConfig(); let config = { ...defaultConfig, ...priorConfig, ...existingData.config }; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index 2557745c9d1..eccc82f3e84 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -114,7 +114,7 @@ const contentPatternToSearchConfiguration = (pattern: ITextQuery, includes: stri wholeWord: !!pattern.contentPattern.isWordMatch, excludes, includes, showIncludesExcludes: !!(includes || excludes || pattern?.userDisabledExcludesAndIgnoreFiles), - useIgnores: !!(pattern?.userDisabledExcludesAndIgnoreFiles === undefined ? undefined : !pattern.userDisabledExcludesAndIgnoreFiles), + useIgnores: (pattern?.userDisabledExcludesAndIgnoreFiles === undefined ? true : !pattern.userDisabledExcludesAndIgnoreFiles), contextLines, }; }; diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index b1deb3354ba..a9e0f87a05b 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -93,15 +93,15 @@ export class Snippet { } get codeSnippet(): string { - return this._bodyInsights.getValue().codeSnippet; + return this._bodyInsights.value.codeSnippet; } get isBogous(): boolean { - return this._bodyInsights.getValue().isBogous; + return this._bodyInsights.value.isBogous; } get needsClipboard(): boolean { - return this._bodyInsights.getValue().needsClipboard; + return this._bodyInsights.value.needsClipboard; } static compare(a: Snippet, b: Snippet): number { diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index de5fcf72cb2..58b81d426c1 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -19,7 +19,7 @@ import * as strings from 'vs/base/common/strings'; import { ValidationStatus, ValidationState } from 'vs/base/common/parsers'; import * as UUID from 'vs/base/common/uuid'; import * as Platform from 'vs/base/common/platform'; -import { LRUCache } from 'vs/base/common/map'; +import { LRUCache, Touch } from 'vs/base/common/map'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IMarkerService } from 'vs/platform/markers/common/markers'; @@ -715,9 +715,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const folderToTasksMap: Map = new Map(); const recentlyUsedTasks = this.getRecentlyUsedTasks(); const tasks: (Task | ConfiguringTask)[] = []; - for (const key of recentlyUsedTasks.keys()) { + for (const entry of recentlyUsedTasks.entries()) { + const key = entry[0]; + const task = JSON.parse(entry[1]); const folder = this.getFolderFromTaskKey(key); - const task = JSON.parse(recentlyUsedTasks.get(key)!); if (folder && !folderToTasksMap.has(folder)) { folderToTasksMap.set(folder, []); } @@ -791,13 +792,13 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (quickOpenHistoryLimit === 0) { return; } - let keys = this._recentlyUsedTasks.keys(); + let keys = [...this._recentlyUsedTasks.keys()]; if (keys.length > quickOpenHistoryLimit) { keys = keys.slice(0, quickOpenHistoryLimit); } const keyValues: [string, string][] = []; for (const key of keys) { - keyValues.push([key, this._recentlyUsedTasks.get(key)!]); + keyValues.push([key, this._recentlyUsedTasks.get(key, Touch.None)!]); } this.storageService.store(AbstractTaskService.RecentlyUsedTasks_KeyV2, JSON.stringify(keyValues), StorageScope.WORKSPACE); } @@ -2325,7 +2326,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer taskMap[key] = task; } }); - const reversed = recentlyUsedTasks.keys().reverse(); + const reversed = [...recentlyUsedTasks.keys()].reverse(); for (const key in reversed) { let task = taskMap[key]; if (task) { diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index cbe96131cb7..4866dd1e823 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -145,7 +145,7 @@ export class ManageAutomaticTaskRunning extends Action { super(id, label); } - public async run(event?: any): Promise { + public async run(): Promise { const allowItem: IQuickPickItem = { label: nls.localize('workbench.action.tasks.allowAutomaticTasks', "Allow Automatic Tasks in Folder") }; const disallowItem: IQuickPickItem = { label: nls.localize('workbench.action.tasks.disallowAutomaticTasks', "Disallow Automatic Tasks in Folder") }; const value = await this.quickInputService.pick([allowItem, disallowItem], { canPickMany: false }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 594bf53132e..dfaef3ea7e4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -236,7 +236,7 @@ export class SplitTerminalAction extends Action { super(id, label, SplitTerminalAction.HORIZONTAL_CLASS); } - public async run(event?: any): Promise { + public async run(): Promise { await this._terminalService.doWithActiveInstance(async t => { const cwd = await getCwdForSplit(this._terminalService.configHelper, t, this._workspaceContextService.getWorkspace().folders, this._commandService); if (cwd === undefined) { diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 937fd714ef0..f369566829c 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -209,7 +209,7 @@ export const terminalConfiguration: IConfigurationNode = { default: false }, 'terminal.integrated.commandsToSkipShell': { - markdownDescription: localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell and instead always be handled by Code. This allows the use of keybindings that would normally be consumed by the shell to act the same as when the terminal is not focused, for example ctrl+p to launch Quick Open.\nDefault Skipped Commands:\n\n{0}", DEFAULT_COMMANDS_TO_SKIP_SHELL.sort().map(command => `- ${command}`).join('\n')), + markdownDescription: localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell and instead always be handled by Code. This allows the use of keybindings that would normally be consumed by the shell to act the same as when the terminal is not focused, for example ctrl+p to launch Quick Open. Use the command prefixed with `-` to remove default commands from the list.\nDefault Skipped Commands:\n\n{0}", DEFAULT_COMMANDS_TO_SKIP_SHELL.sort().map(command => `- ${command}`).join('\n')), type: 'array', items: { type: 'string' diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalRemote.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalRemote.ts index bf4e6c98fbf..1547e863513 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalRemote.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalRemote.ts @@ -29,7 +29,7 @@ export class CreateNewLocalTerminalAction extends Action { super(id, label); } - public run(event?: any): Promise { + public run(): Promise { const instance = this.terminalService.createTerminal({ cwd: URI.file(homedir()) }); if (!instance) { return Promise.resolve(undefined); diff --git a/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css b/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css new file mode 100644 index 00000000000..4210055bfeb --- /dev/null +++ b/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.file-icons-enabled .show-file-icons .webview-vs_code_release_notes-name-file-icon.file-icon::before { + content: ' '; + background-image: url('../../../../browser/media/code-icon.svg'); +} diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 58db1270719..b8ce8995fde 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/releasenoteseditor'; import { onUnexpectedError } from 'vs/base/common/errors'; import { OS } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; @@ -74,7 +75,7 @@ export class ReleaseNotesManager { this._webviewWorkbenchService.revealWebview(this._currentReleaseNotes, activeEditorPane ? activeEditorPane.group : this._editorGroupService.activeGroup, false); } else { this._currentReleaseNotes = this._webviewWorkbenchService.createWebview( - generateUuid(), + 'vs_code_release_notes', 'releaseNotes', title, { group: ACTIVE_GROUP, preserveFocus: false }, @@ -88,11 +89,6 @@ export class ReleaseNotesManager { this._currentReleaseNotes.webview.onDidClickLink(uri => this.onDidClickLink(URI.parse(uri))); this._currentReleaseNotes.onDispose(() => { this._currentReleaseNotes = undefined; }); - const iconPath = URI.parse(require.toUrl('vs/workbench/browser/media/code-icon.svg')); - this._currentReleaseNotes.iconPath = { - light: iconPath, - dark: iconPath - }; this._currentReleaseNotes.webview.html = html; } diff --git a/src/vs/workbench/contrib/url/common/externalUriResolver.ts b/src/vs/workbench/contrib/url/browser/externalUriResolver.ts similarity index 100% rename from src/vs/workbench/contrib/url/common/externalUriResolver.ts rename to src/vs/workbench/contrib/url/browser/externalUriResolver.ts diff --git a/src/vs/workbench/contrib/url/common/trustedDomains.ts b/src/vs/workbench/contrib/url/browser/trustedDomains.ts similarity index 68% rename from src/vs/workbench/contrib/url/common/trustedDomains.ts rename to src/vs/workbench/contrib/url/browser/trustedDomains.ts index caa31e673c7..c76b9d9d5b6 100644 --- a/src/vs/workbench/contrib/url/common/trustedDomains.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomains.ts @@ -11,6 +11,10 @@ import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/commo import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; const TRUSTED_DOMAINS_URI = URI.parse('trustedDomains:/Trusted Domains'); @@ -116,7 +120,42 @@ export async function configureOpenerTrustedDomainsHandler( return []; } -export function readTrustedDomains(storageService: IStorageService, productService: IProductService) { +async function getRemotes(fileService: IFileService, textFileService: ITextFileService, contextService: IWorkspaceContextService): Promise { + const workspaceUris = contextService.getWorkspace().folders.map(folder => folder.uri); + const domains = await Promise.all(workspaceUris.map(async workspaceUri => { + const path = workspaceUri.path; + const uri = workspaceUri.with({ path: `${path !== '/' ? path : ''}/.git/config` }); + const exists = await fileService.exists(uri); + if (!exists) { + return []; + } + const content = (await (textFileService.read(uri, { acceptTextOnly: true }).catch(() => ({ value: '' })))).value; + const domains = new Set(); + let match: RegExpExecArray | null; + + const RemoteMatcher = /^\s*url\s*=\s*(?:git@|https:\/\/)github\.com(?::|\/)([^.]*)\.git\s*$/mg; + while (match = RemoteMatcher.exec(content)) { + const [domain, repo] = [match[1], match[2]]; + if (domain && repo) { + domains.add(`https://github.com/${repo}/`); + } + } + return [...domains]; + })); + + const set = domains.reduce((set, list) => list.reduce((set, item) => set.add(item), set), new Set()); + return [...set]; +} + +export async function readTrustedDomains(accessor: ServicesAccessor) { + + const storageService = accessor.get(IStorageService); + const productService = accessor.get(IProductService); + const authenticationService = accessor.get(IAuthenticationService); + const fileService = accessor.get(IFileService); + const textFileService = accessor.get(ITextFileService); + const workspaceContextService = accessor.get(IWorkspaceContextService); + const defaultTrustedDomains: string[] = productService.linkProtectionTrustedDomains ? [...productService.linkProtectionTrustedDomains] : []; @@ -129,8 +168,16 @@ export function readTrustedDomains(storageService: IStorageService, productServi } } catch (err) { } + const userDomains = ((await authenticationService.getSessions('github')) ?? []) + .map(session => session.account.displayName) + .map(username => `https://github.com/${username}/`); + + const workspaceDomains = await getRemotes(fileService, textFileService, workspaceContextService); + return { defaultTrustedDomains, - trustedDomains + trustedDomains, + userDomains, + workspaceDomains }; } diff --git a/src/vs/workbench/contrib/url/common/trustedDomainsFileSystemProvider.ts b/src/vs/workbench/contrib/url/browser/trustedDomainsFileSystemProvider.ts similarity index 78% rename from src/vs/workbench/contrib/url/common/trustedDomainsFileSystemProvider.ts rename to src/vs/workbench/contrib/url/browser/trustedDomainsFileSystemProvider.ts index aba030acc3c..b34f0b5ba6f 100644 --- a/src/vs/workbench/contrib/url/common/trustedDomainsFileSystemProvider.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainsFileSystemProvider.ts @@ -11,9 +11,10 @@ import { FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { VSBuffer } from 'vs/base/common/buffer'; -import { readTrustedDomains, TRUSTED_DOMAINS_CONTENT_STORAGE_KEY, TRUSTED_DOMAINS_STORAGE_KEY } from 'vs/workbench/contrib/url/common/trustedDomains'; -import { IProductService } from 'vs/platform/product/common/productService'; +import { readTrustedDomains, TRUSTED_DOMAINS_CONTENT_STORAGE_KEY, TRUSTED_DOMAINS_STORAGE_KEY } from 'vs/workbench/contrib/url/browser/trustedDomains'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { assertIsDefined } from 'vs/base/common/types'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; const TRUSTED_DOMAINS_SCHEMA = 'trustedDomains'; @@ -45,7 +46,7 @@ const CONFIG_PLACEHOLDER_TEXT = `[ // "https://microsoft.com" ]`; -function computeTrustedDomainContent(defaultTrustedDomains: string[], trustedDomains: string[]) { +function computeTrustedDomainContent(defaultTrustedDomains: string[], trustedDomains: string[], userTrustedDomains: string[], workspaceTrustedDomains: string[]) { let content = CONFIG_HELP_TEXT_PRE; if (defaultTrustedDomains.length > 0) { @@ -57,6 +58,20 @@ function computeTrustedDomainContent(defaultTrustedDomains: string[], trustedDom content += `// By default, VS Code trusts "localhost".\n`; } + if (userTrustedDomains.length) { + content += `//\n// Additionally, the following domains are trusted based on your current GitHub login:\n`; + userTrustedDomains.forEach(d => { + content += `// - "${d}"\n`; + }); + } + + if (workspaceTrustedDomains.length) { + content += `//\n// Further, the following domains are trusted based on your workspace configuration:\n`; + workspaceTrustedDomains.forEach(d => { + content += `// - "${d}"\n`; + }); + } + content += CONFIG_HELP_TEXT_AFTER; if (trustedDomains.length === 0) { @@ -77,8 +92,8 @@ export class TrustedDomainsFileSystemProvider implements IFileSystemProviderWith constructor( @IFileService private readonly fileService: IFileService, @IStorageService private readonly storageService: IStorageService, - @IProductService private readonly productService: IProductService, - @IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { this.fileService.registerProvider(TRUSTED_DOMAINS_SCHEMA, this); @@ -90,24 +105,24 @@ export class TrustedDomainsFileSystemProvider implements IFileSystemProviderWith return Promise.resolve(TRUSTED_DOMAINS_STAT); } - readFile(resource: URI): Promise { + async readFile(resource: URI): Promise { let trustedDomainsContent = this.storageService.get( TRUSTED_DOMAINS_CONTENT_STORAGE_KEY, StorageScope.GLOBAL ); + const { defaultTrustedDomains, trustedDomains, userDomains, workspaceDomains } = await this.instantiationService.invokeFunction(readTrustedDomains); if ( !trustedDomainsContent || trustedDomainsContent.indexOf(CONFIG_HELP_TEXT_PRE) === -1 || - trustedDomainsContent.indexOf(CONFIG_HELP_TEXT_AFTER) === -1 + trustedDomainsContent.indexOf(CONFIG_HELP_TEXT_AFTER) === -1 || + [...defaultTrustedDomains, ...trustedDomains, ...userDomains, ...workspaceDomains].some(d => !assertIsDefined(trustedDomainsContent).includes(d)) ) { - const { defaultTrustedDomains, trustedDomains } = readTrustedDomains(this.storageService, this.productService); - - trustedDomainsContent = computeTrustedDomainContent(defaultTrustedDomains, trustedDomains); + trustedDomainsContent = computeTrustedDomainContent(defaultTrustedDomains, trustedDomains, userDomains, workspaceDomains); } const buffer = VSBuffer.fromString(trustedDomainsContent).buffer; - return Promise.resolve(buffer); + return buffer; } writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { diff --git a/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts similarity index 94% rename from src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts rename to src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts index bfb96b17c42..444de702e7a 100644 --- a/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts @@ -16,10 +16,11 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { configureOpenerTrustedDomainsHandler, readTrustedDomains -} from 'vs/workbench/contrib/url/common/trustedDomains'; +} from 'vs/workbench/contrib/url/browser/trustedDomains'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; type TrustedDomainsDialogActionClassification = { action: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; @@ -34,7 +35,8 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { @IQuickInputService private readonly _quickInputService: IQuickInputService, @IEditorService private readonly _editorService: IEditorService, @IClipboardService private readonly _clipboardService: IClipboardService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) }); } @@ -50,8 +52,8 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { const { scheme, authority, path, query, fragment } = resource; const domainToOpen = `${scheme}://${authority}`; - const { defaultTrustedDomains, trustedDomains } = readTrustedDomains(this._storageService, this._productService); - const allTrustedDomains = [...defaultTrustedDomains, ...trustedDomains]; + const { defaultTrustedDomains, trustedDomains, userDomains, workspaceDomains } = await this._instantiationService.invokeFunction(readTrustedDomains); + const allTrustedDomains = [...defaultTrustedDomains, ...trustedDomains, ...userDomains, ...workspaceDomains]; if (isURLDomainTrusted(resource, allTrustedDomains)) { return true; diff --git a/src/vs/workbench/contrib/url/common/url.contribution.ts b/src/vs/workbench/contrib/url/browser/url.contribution.ts similarity index 94% rename from src/vs/workbench/contrib/url/common/url.contribution.ts rename to src/vs/workbench/contrib/url/browser/url.contribution.ts index 5da6346c473..aba83696703 100644 --- a/src/vs/workbench/contrib/url/common/url.contribution.ts +++ b/src/vs/workbench/contrib/url/browser/url.contribution.ts @@ -14,10 +14,10 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IURLService } from 'vs/platform/url/common/url'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { ExternalUriResolverContribution } from 'vs/workbench/contrib/url/common/externalUriResolver'; -import { manageTrustedDomainSettingsCommand } from 'vs/workbench/contrib/url/common/trustedDomains'; -import { TrustedDomainsFileSystemProvider } from 'vs/workbench/contrib/url/common/trustedDomainsFileSystemProvider'; -import { OpenerValidatorContributions } from 'vs/workbench/contrib/url/common/trustedDomainsValidator'; +import { ExternalUriResolverContribution } from 'vs/workbench/contrib/url/browser/externalUriResolver'; +import { manageTrustedDomainSettingsCommand } from 'vs/workbench/contrib/url/browser/trustedDomains'; +import { TrustedDomainsFileSystemProvider } from 'vs/workbench/contrib/url/browser/trustedDomainsFileSystemProvider'; +import { OpenerValidatorContributions } from 'vs/workbench/contrib/url/browser/trustedDomainsValidator'; export class OpenUrlAction extends Action { static readonly ID = 'workbench.action.url.openUrl'; diff --git a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts index 8b69baf8039..61c77b78bea 100644 --- a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts +++ b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; -import { isURLDomainTrusted } from 'vs/workbench/contrib/url/common/trustedDomainsValidator'; +import { isURLDomainTrusted } from 'vs/workbench/contrib/url/browser/trustedDomainsValidator'; import { URI } from 'vs/base/common/uri'; function linkAllowedByRules(link: string, rules: string[]) { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index ab1e8ee0f4e..ba8bcf7c7e7 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -79,7 +79,7 @@ const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolv const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Preferences Sync: Show Keybindings Conflicts") }; const resolveSnippetsConflictsCommand = { id: 'workbench.userData.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Preferences Sync: Show User Snippets Conflicts") }; const configureSyncCommand = { id: 'workbench.userData.actions.configureSync', title: localize('configure sync', "Preferences Sync: Configure...") }; -const showSyncActivityCommand = { id: 'workbench.userData.actions.showSyncActivity', title: localize('show sync log', "Preferences Sync: Show Log") }; +const showSyncActivityCommand = { id: 'workbench.userData.actions.showSyncActivity', title: localize('show sync log title', "Preferences Sync: Show Log") }; const syncNowCommand = { id: 'workbench.userData.actions.syncNow', title: localize('sync now', "Preferences Sync: Sync Now"), diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 4d37537fa17..bfe8903d4d6 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -21,6 +21,7 @@ export const enum WebviewMessageChannels { didScroll = 'did-scroll', didFocus = 'did-focus', didBlur = 'did-blur', + didLoad = 'did-load', doUpdateState = 'do-update-state', doReload = 'do-reload', loadResource = 'load-resource', @@ -153,6 +154,9 @@ export abstract class BaseWebview extends Disposable { private readonly _onDidClickLink = this._register(new Emitter()); public readonly onDidClickLink = this._onDidClickLink.event; + private readonly _onDidReload = this._register(new Emitter()); + public readonly onDidReload = this._onDidReload.event; + private readonly _onMessage = this._register(new Emitter()); public readonly onMessage = this._onMessage.event; @@ -216,6 +220,10 @@ export abstract class BaseWebview extends Disposable { public reload(): void { this.doUpdateContent(); + const subscription = this._register(this.on(WebviewMessageChannels.didLoad, () => { + this._onDidReload.fire(); + subscription.dispose(); + })); } public set html(value: string) { diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index c40464378d4..2ce69211f98 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -130,6 +130,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv this._webviewEvents.add(webview.onMessage(x => { this._onMessage.fire(x); })); this._webviewEvents.add(webview.onMissingCsp(x => { this._onMissingCsp.fire(x); })); this._webviewEvents.add(webview.onDidWheel(x => { this._onDidWheel.fire(x); })); + this._webviewEvents.add(webview.onDidReload(() => { this._onDidReload.fire(); })); this._webviewEvents.add(webview.onDidScroll(x => { this._initialScrollProgress = x.scrollYPercentage; @@ -189,6 +190,9 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv private readonly _onDidClickLink = this._register(new Emitter()); public readonly onDidClickLink: Event = this._onDidClickLink.event; + private readonly _onDidReload = this._register(new Emitter()); + public readonly onDidReload = this._onDidReload.event; + private readonly _onDidScroll = this._register(new Emitter<{ scrollYPercentage: number; }>()); public readonly onDidScroll: Event<{ scrollYPercentage: number; }> = this._onDidScroll.event; diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index f2ca5011ddc..8fc84cd45df 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -517,6 +517,8 @@ }); pendingMessages = []; } + + host.postMessage('did-load'); }; /** diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index e592683896b..610f20272be 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -84,6 +84,7 @@ export interface Webview extends IDisposable { readonly onDidScroll: Event<{ scrollYPercentage: number }>; readonly onDidWheel: Event; readonly onDidUpdateState: Event; + readonly onDidReload: Event; readonly onMessage: Event; readonly onMissingCsp: Event; diff --git a/src/vs/workbench/contrib/webview/common/themeing.ts b/src/vs/workbench/contrib/webview/common/themeing.ts index cd8e73fafc4..5d66fb268ef 100644 --- a/src/vs/workbench/contrib/webview/common/themeing.ts +++ b/src/vs/workbench/contrib/webview/common/themeing.ts @@ -63,7 +63,7 @@ export class WebviewThemeDataProvider extends Disposable { }, {} as { [key: string]: string; }); const styles = { - 'vscode-font-family': '-apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif', + 'vscode-font-family': 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif', 'vscode-font-weight': 'normal', 'vscode-font-size': '13px', 'vscode-editor-font-family': editorFontFamily, diff --git a/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js b/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js index 8be7c9520a6..e87ab16dba4 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js +++ b/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js @@ -79,4 +79,4 @@ }); require('../../browser/pre/main')(host); -}()); \ No newline at end of file +}()); diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 573b4f0f546..6a76e88e019 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -543,7 +543,7 @@ export class ConfigurationEditingService { const standaloneConfigurationMap = target === EditableConfigurationTarget.USER_LOCAL ? USER_STANDALONE_CONFIGURATIONS : WORKSPACE_STANDALONE_CONFIGURATIONS; const standaloneConfigurationKeys = Object.keys(standaloneConfigurationMap); for (const key of standaloneConfigurationKeys) { - const resource = this.getConfigurationFileResource(target, config, standaloneConfigurationMap[key], overrides.resource); + const resource = this.getConfigurationFileResource(target, standaloneConfigurationMap[key], overrides.resource); // Check for prefix if (config.key === key) { @@ -563,10 +563,10 @@ export class ConfigurationEditingService { let key = config.key; let jsonPath = overrides.overrideIdentifier ? [keyFromOverrideIdentifier(overrides.overrideIdentifier), key] : [key]; if (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE) { - return { key, jsonPath, value: config.value, resource: withNullAsUndefined(this.getConfigurationFileResource(target, config, '', null)), target }; + return { key, jsonPath, value: config.value, resource: withNullAsUndefined(this.getConfigurationFileResource(target, '', null)), target }; } - const resource = this.getConfigurationFileResource(target, config, FOLDER_SETTINGS_PATH, overrides.resource); + const resource = this.getConfigurationFileResource(target, FOLDER_SETTINGS_PATH, overrides.resource); if (this.isWorkspaceConfigurationResource(resource)) { jsonPath = ['settings', ...jsonPath]; } @@ -578,7 +578,7 @@ export class ConfigurationEditingService { return !!(workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath); } - private getConfigurationFileResource(target: EditableConfigurationTarget, config: IConfigurationValue, relativePath: string, resource: URI | null | undefined): URI | null { + private getConfigurationFileResource(target: EditableConfigurationTarget, relativePath: string, resource: URI | null | undefined): URI | null { if (target === EditableConfigurationTarget.USER_LOCAL) { if (relativePath) { return resources.joinPath(resources.dirname(this.environmentService.settingsResource), relativePath); diff --git a/src/vs/workbench/services/credentials/node/credentialsService.ts b/src/vs/workbench/services/credentials/node/credentialsService.ts deleted file mode 100644 index 8c876d5b759..00000000000 --- a/src/vs/workbench/services/credentials/node/credentialsService.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; -import { IdleValue } from 'vs/base/common/async'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; - -type KeytarModule = typeof import('keytar'); -export class KeytarCredentialsService implements ICredentialsService { - - _serviceBrand: undefined; - - private readonly _keytar = new IdleValue>(() => import('keytar')); - - async getPassword(service: string, account: string): Promise { - const keytar = await this._keytar.getValue(); - return keytar.getPassword(service, account); - } - - async setPassword(service: string, account: string, password: string): Promise { - const keytar = await this._keytar.getValue(); - return keytar.setPassword(service, account, password); - } - - async deletePassword(service: string, account: string): Promise { - const keytar = await this._keytar.getValue(); - return keytar.deletePassword(service, account); - } - - async findPassword(service: string): Promise { - const keytar = await this._keytar.getValue(); - return keytar.findPassword(service); - } - - async findCredentials(service: string): Promise> { - const keytar = await this._keytar.getValue(); - return keytar.findCredentials(service); - } -} - -registerSingleton(ICredentialsService, KeytarCredentialsService, true); diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index ea3c10d78a1..25d1d959bb5 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -359,6 +359,7 @@ export const enum GroupChangeKind { EDITOR_ACTIVE, EDITOR_LABEL, EDITOR_PIN, + EDITOR_STICKY, EDITOR_DIRTY } diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index d929a5c5338..42bc6704225 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -369,8 +369,9 @@ suite('EditorGroupsService', () => { let activeEditorChangeCounter = 0; let editorDidOpenCounter = 0; - let editorCloseCounter1 = 0; + let editorCloseCounter = 0; let editorPinCounter = 0; + let editorStickyCounter = 0; const editorGroupChangeListener = group.onDidGroupChange(e => { if (e.kind === GroupChangeKind.EDITOR_OPEN) { assert.ok(e.editor); @@ -380,16 +381,19 @@ suite('EditorGroupsService', () => { activeEditorChangeCounter++; } else if (e.kind === GroupChangeKind.EDITOR_CLOSE) { assert.ok(e.editor); - editorCloseCounter1++; + editorCloseCounter++; } else if (e.kind === GroupChangeKind.EDITOR_PIN) { assert.ok(e.editor); editorPinCounter++; + } else if (e.kind === GroupChangeKind.EDITOR_STICKY) { + assert.ok(e.editor); + editorStickyCounter++; } }); - let editorCloseCounter2 = 0; + let editorCloseCounter1 = 0; const editorCloseListener = group.onDidCloseEditor(() => { - editorCloseCounter2++; + editorCloseCounter1++; }); let editorWillCloseCounter = 0; @@ -440,12 +444,18 @@ suite('EditorGroupsService', () => { await group.closeEditor(inputInactive); assert.equal(activeEditorChangeCounter, 3); + assert.equal(editorCloseCounter, 1); assert.equal(editorCloseCounter1, 1); - assert.equal(editorCloseCounter2, 1); assert.equal(editorWillCloseCounter, 1); assert.equal(group.activeEditor, input); + assert.equal(editorStickyCounter, 0); + group.stickEditor(input); + assert.equal(editorStickyCounter, 1); + group.unstickEditor(input); + assert.equal(editorStickyCounter, 2); + editorCloseListener.dispose(); editorWillCloseListener.dispose(); editorWillOpenListener.dispose(); diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index fd7b5856896..88f766b777b 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -614,7 +614,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten } } else { // Install the Extension and reload the window to handle. - const message = nls.localize('installResolver', "Extension '{0}' is required to open the remote window.\nOK to install?", recommendation.friendlyName); + const message = nls.localize('installResolver', "Extension '{0}' is required to open the remote window.\nDo you want to install the extension?", recommendation.friendlyName); this._notificationService.prompt(Severity.Info, message, [{ label: nls.localize('install', 'Install and Reload'), diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 9d76cd2dbaa..58a51270185 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -5,7 +5,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IEditor } from 'vs/editor/common/editorCommon'; -import { ITextEditorOptions, IResourceEditorInput, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { ITextEditorOptions, IResourceEditorInput, TextEditorSelectionRevealType, IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorInput, IEditorPane, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, EditorsOrder } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; @@ -638,10 +638,22 @@ export class HistoryService extends Disposable implements IHistoryService { if (lastClosedFile) { (async () => { - const editor = await this.editorService.openEditor({ - resource: lastClosedFile.resource, - options: { pinned: true, sticky: lastClosedFile.sticky, index: lastClosedFile.index } - }); + let options: IEditorOptions; + if (lastClosedFile.sticky) { + // Sticky: in case the target index is outside of the range of + // sticky editors, we make sure to not provide the index as + // option. Otherwise the index will cause the sticky flag to + // be ignored. + if (!this.editorGroupService.activeGroup.isSticky(lastClosedFile.index)) { + options = { pinned: true, sticky: true }; + } else { + options = { pinned: true, sticky: true, index: lastClosedFile.index }; + } + } else { + options = { pinned: true, index: lastClosedFile.index }; + } + + const editor = await this.editorService.openEditor({ resource: lastClosedFile.resource, options }); // Fix for https://github.com/Microsoft/vscode/issues/67882 // If opening of the editor fails, make sure to try the next one diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 45f02fdcfb0..e3366db8981 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -65,7 +65,7 @@ export class BrowserHostService extends Disposable implements IHostService { @IConfigurationService private readonly configurationService: IConfigurationService, @IFileService private readonly fileService: IFileService, @ILabelService private readonly labelService: ILabelService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { super(); @@ -113,38 +113,21 @@ export class BrowserHostService extends Disposable implements IHostService { } private async doOpenWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise { + const payload = this.preservePayload(); + for (let i = 0; i < toOpen.length; i++) { const openable = toOpen[i]; openable.label = openable.label || this.getRecentLabel(openable); - // selectively copy payload: for now only extension debugging properties are considered - const originalPayload = this.workspaceProvider.payload; - let newPayload: Array | undefined = undefined; - if (originalPayload && Array.isArray(originalPayload)) { - for (let pair of originalPayload) { - if (Array.isArray(pair) && pair.length === 2) { - switch (pair[0]) { - case 'extensionDevelopmentPath': - case 'debugId': - case 'inspect-brk-extensions': - if (!newPayload) { - newPayload = new Array(); - } - newPayload.push(pair); - break; - } - } - } - } // Folder if (isFolderToOpen(openable)) { - this.workspaceProvider.open({ folderUri: openable.folderUri }, { reuse: this.shouldReuse(options, false /* no file */), payload: newPayload }); + this.workspaceProvider.open({ folderUri: openable.folderUri }, { reuse: this.shouldReuse(options, false /* no file */), payload }); } // Workspace else if (isWorkspaceToOpen(openable)) { - this.workspaceProvider.open({ workspaceUri: openable.workspaceUri }, { reuse: this.shouldReuse(options, false /* no file */), payload: newPayload }); + this.workspaceProvider.open({ workspaceUri: openable.workspaceUri }, { reuse: this.shouldReuse(options, false /* no file */), payload }); } // File @@ -167,6 +150,27 @@ export class BrowserHostService extends Disposable implements IHostService { } } + private preservePayload(): Array | undefined { + + // Selectively copy payload: for now only extension debugging properties are considered + let newPayload: Array | undefined = undefined; + if (this.environmentService.extensionDevelopmentLocationURI) { + newPayload = new Array(); + + newPayload.push(['extensionDevelopmentPath', this.environmentService.extensionDevelopmentLocationURI.toString()]); + + if (this.environmentService.debugExtensionHost.debugId) { + newPayload.push(['debugId', this.environmentService.debugExtensionHost.debugId]); + } + + if (this.environmentService.debugExtensionHost.port) { + newPayload.push(['inspect-brk-extensions', String(this.environmentService.debugExtensionHost.port)]); + } + } + + return newPayload; + } + private getRecentLabel(openable: IWindowOpenable): string { if (isFolderToOpen(openable)) { return this.labelService.getWorkspaceLabel(openable.folderUri, { verbose: true }); diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index bc872a3633e..6334bcafbf6 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -518,7 +518,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this._defaultUserSettingsContentModel; } - private async getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): Promise { + public async getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): Promise { switch (configurationTarget) { case ConfigurationTarget.USER: case ConfigurationTarget.USER_LOCAL: diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index e39388c5e71..f7795c59a86 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -207,6 +207,7 @@ export interface IPreferencesService { switchSettings(target: ConfigurationTarget, resource: URI, jsonEditor?: boolean): Promise; openGlobalKeybindingSettings(textual: boolean): Promise; openDefaultKeybindingsFile(): Promise; + getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): Promise; } export function getSettingsTargetName(target: ConfigurationTarget, resource: URI, workspaceContextService: IWorkspaceContextService): string { diff --git a/src/vs/workbench/services/preferences/common/preferencesValidation.ts b/src/vs/workbench/services/preferences/common/preferencesValidation.ts index 9a1080649a6..6a205333596 100644 --- a/src/vs/workbench/services/preferences/common/preferencesValidation.ts +++ b/src/vs/workbench/services/preferences/common/preferencesValidation.ts @@ -55,31 +55,41 @@ export function createValidator(prop: IConfigurationPropertySchema): (value: any }; } +/** + * Returns an error string if the value is invalid and can't be displayed in the settings UI for the given type. + */ export function getInvalidTypeError(value: any, type: undefined | string | string[]): string | undefined { - let typeArr = Array.isArray(type) ? type : [type]; - const isNullable = canBeType(typeArr, 'null'); - if (canBeType(typeArr, 'number', 'integer') && (typeArr.length === 1 || typeArr.length === 2 && isNullable)) { - if (value === '' || isNaN(+value)) { - return nls.localize('validations.expectedNumeric', "Value must be a number."); - } + if (typeof type === 'undefined') { + return; } - const valueType = typeof value; - if ( - (valueType === 'boolean' && !canBeType(typeArr, 'boolean')) || - (valueType === 'object' && !canBeType(typeArr, 'object', 'null', 'array')) || - (valueType === 'string' && !canBeType(typeArr, 'string', 'number', 'integer')) || - (typeof parseFloat(value) === 'number' && !isNaN(parseFloat(value)) && !canBeType(typeArr, 'number', 'integer')) || - (Array.isArray(value) && !canBeType(typeArr, 'array')) - ) { - if (typeof type !== 'undefined') { - return nls.localize('invalidTypeError', "Setting has an invalid type, expected {0}. Fix in JSON.", JSON.stringify(type)); - } + const typeArr = Array.isArray(type) ? type : [type]; + if (!typeArr.some(_type => valueValidatesAsType(value, _type))) { + return nls.localize('invalidTypeError', "Setting has an invalid type, expected {0}. Fix in JSON.", JSON.stringify(type)); } return; } +function valueValidatesAsType(value: any, type: string): boolean { + const valueType = typeof value; + if (type === 'boolean') { + return valueType === 'boolean'; + } else if (type === 'object') { + return value && !Array.isArray(value) && valueType === 'object'; + } else if (type === 'null') { + return value === null; + } else if (type === 'array') { + return Array.isArray(value); + } else if (type === 'string') { + return valueType === 'string'; + } else if (type === 'number' || type === 'integer') { + return valueType === 'number'; + } + + return true; +} + function getStringValidators(prop: IConfigurationPropertySchema) { let patternRegex: RegExp | undefined; if (typeof prop.pattern === 'string') { diff --git a/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts b/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts similarity index 86% rename from src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts rename to src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts index a9ff80cc2b9..9fcbd932494 100644 --- a/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts +++ b/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts @@ -5,10 +5,10 @@ import * as assert from 'assert'; import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; -import { createValidator } from 'vs/workbench/services/preferences/common/preferencesValidation'; +import { createValidator, getInvalidTypeError } from 'vs/workbench/services/preferences/common/preferencesValidation'; -suite('Preferences Model test', () => { +suite('Preferences Validation', () => { class Tester { private validator: (value: any) => string | null; @@ -334,4 +334,43 @@ suite('Preferences Model test', () => { arr.rejects(['a', 'a']).withMessage(`Array has duplicate items`); }); + + test('getInvalidTypeError', () => { + function testInvalidTypeError(value: any, type: string | string[], shouldValidate: boolean) { + const message = `value: ${value}, type: ${JSON.stringify(type)}, expected: ${shouldValidate ? 'valid' : 'invalid'}`; + if (shouldValidate) { + assert.ok(!getInvalidTypeError(value, type), message); + } else { + assert.ok(getInvalidTypeError(value, type), message); + } + } + + testInvalidTypeError(1, 'number', true); + testInvalidTypeError(1.5, 'number', true); + testInvalidTypeError([1], 'number', false); + testInvalidTypeError('1', 'number', false); + testInvalidTypeError({ a: 1 }, 'number', false); + testInvalidTypeError(null, 'number', false); + + testInvalidTypeError('a', 'string', true); + testInvalidTypeError('1', 'string', true); + testInvalidTypeError([], 'string', false); + testInvalidTypeError({}, 'string', false); + + testInvalidTypeError([1], 'array', true); + testInvalidTypeError([], 'array', true); + testInvalidTypeError([{}, [[]]], 'array', true); + testInvalidTypeError({ a: ['a'] }, 'array', false); + testInvalidTypeError('hello', 'array', false); + + testInvalidTypeError(true, 'boolean', true); + testInvalidTypeError('hello', 'boolean', false); + testInvalidTypeError(null, 'boolean', false); + testInvalidTypeError([true], 'boolean', false); + + testInvalidTypeError(null, 'null', true); + testInvalidTypeError(false, 'null', false); + testInvalidTypeError([null], 'null', false); + testInvalidTypeError('null', 'null', false); + }); }); diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 19af00e19f8..806f1706358 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -183,7 +183,7 @@ export function resultIsMatch(result: ITextSearchResult): result is ITextSearchM } export interface IProgressMessage { - message?: string; + message: string; } export type ISearchProgressItem = IFileMatch | IProgressMessage; @@ -192,8 +192,8 @@ export function isFileMatch(p: ISearchProgressItem): p is IFileMatch { return !!(p).resource; } -export function isProgressMessage(p: ISearchProgressItem): p is IProgressMessage { - return !isFileMatch(p); +export function isProgressMessage(p: ISearchProgressItem | ISerializedSearchProgressItem): p is IProgressMessage { + return !!(p as IProgressMessage).message; } export interface ISearchCompleteStats { @@ -349,7 +349,8 @@ export interface ISearchConfigurationProperties { searchOnTypeDebouncePeriod: number; searchEditor: { doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide', - experimental: { reusePriorSearchConfiguration: boolean } + reusePriorSearchConfiguration: boolean, + experimental: {} }; sortOrder: SearchSortOrder; } @@ -467,7 +468,7 @@ export interface IRawFileMatch { * * If not given, the search algorithm should use `relativePath`. */ - searchPath?: string; + searchPath: string | undefined; } export interface ISearchEngine { diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index 4c44030ae27..cec7149d6fe 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -116,41 +116,43 @@ export class SearchService extends Disposable implements ISearchService { schemesInQuery.forEach(scheme => providerActivations.push(this.extensionService.activateByEvent(`onSearch:${scheme}`))); providerActivations.push(this.extensionService.activateByEvent('onSearch:file')); - const providerPromise = Promise.all(providerActivations) - .then(() => this.extensionService.whenInstalledExtensionsRegistered()) - .then(() => { - // Cancel faster if search was canceled while waiting for extensions + const providerPromise = (async () => { + await Promise.all(providerActivations); + this.extensionService.whenInstalledExtensionsRegistered(); + + // Cancel faster if search was canceled while waiting for extensions + if (token && token.isCancellationRequested) { + return Promise.reject(canceled()); + } + + const progressCallback = (item: ISearchProgressItem) => { if (token && token.isCancellationRequested) { - return Promise.reject(canceled()); + return; } - const progressCallback = (item: ISearchProgressItem) => { - if (token && token.isCancellationRequested) { - return; - } - - if (onProgress) { - onProgress(item); - } - }; - - return this.searchWithProviders(query, progressCallback, token); - }) - .then(completes => { - completes = arrays.coalesce(completes); - if (!completes.length) { - return { - limitHit: false, - results: [] - }; + if (onProgress) { + onProgress(item); } + }; - return { - limitHit: completes[0] && completes[0].limitHit, - stats: completes[0].stats, - results: arrays.flatten(completes.map((c: ISearchComplete) => c.results)) + const exists = await Promise.all(query.folderQueries.map(query => this.fileService.exists(query.folder))); + query.folderQueries = query.folderQueries.filter((_, i) => exists[i]); + + let completes = await this.searchWithProviders(query, progressCallback, token); + completes = arrays.coalesce(completes); + if (!completes.length) { + return { + limitHit: false, + results: [] }; - }); + } + + return { + limitHit: completes[0] && completes[0].limitHit, + stats: completes[0].stats, + results: arrays.flatten(completes.map((c: ISearchComplete) => c.results)) + }; + })(); return new Promise((resolve, reject) => { if (token) { diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 97fe8231b86..719ad873b50 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -24,9 +24,8 @@ import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFil import { spawnRipgrepCmd } from './ripgrepFileSearch'; import { prepareQuery } from 'vs/base/common/fuzzyScorer'; -interface IDirectoryEntry { +interface IDirectoryEntry extends IRawFileMatch { base: string; - relativePath: string; basename: string; } @@ -122,7 +121,7 @@ export class FileWalker { } // File: Check for match on file pattern and include pattern - this.matchFile(onResult, { relativePath: extraFilePath.fsPath /* no workspace relative path */ }); + this.matchFile(onResult, { relativePath: extraFilePath.fsPath /* no workspace relative path */, searchPath: undefined }); }); this.cmdSW = StopWatch.create(false); @@ -260,7 +259,7 @@ export class FileWalker { } // TODO: Optimize siblings clauses with ripgrep here. - this.addDirectoryEntries(tree, rootFolder, relativeFiles, onResult); + this.addDirectoryEntries(folderQuery, tree, rootFolder, relativeFiles, onResult); if (last) { this.matchDirectoryTree(tree, rootFolder, onResult); @@ -389,13 +388,17 @@ export class FileWalker { return tree; } - private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) { + private addDirectoryEntries(folderQuery: IFolderQuery, { pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) { // Support relative paths to files from a root resource (ignores excludes) if (relativeFiles.indexOf(this.filePattern) !== -1) { - this.matchFile(onResult, { base: base, relativePath: this.filePattern }); + this.matchFile(onResult, { + base, + relativePath: this.filePattern, + searchPath: this.getSearchPath(folderQuery, this.filePattern) + }); } - function add(relativePath: string) { + const add = (relativePath: string) => { const basename = path.basename(relativePath); const dirname = path.dirname(relativePath); let entries = pathToEntries[dirname]; @@ -406,9 +409,10 @@ export class FileWalker { entries.push({ base, relativePath, - basename + basename, + searchPath: this.getSearchPath(folderQuery, relativePath), }); - } + }; relativeFiles.forEach(add); } diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index 53cfa0b9f15..a9170133473 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -51,7 +51,6 @@ export class DiskSearch implements ISearchResultProvider { searchDebug: IDebugParams | undefined, @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configService: IConfigurationService, - @IFileService private readonly fileService: IFileService ) { const timeout = this.configService.getValue().search.maintainFileSearchCache ? Number.MAX_VALUE : @@ -91,41 +90,31 @@ export class DiskSearch implements ISearchResultProvider { } textSearch(query: ITextQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): Promise { - const folderQueries = query.folderQueries || []; - return Promise.all(folderQueries.map(q => this.fileService.exists(q.folder))) - .then(exists => { - if (token && token.isCancellationRequested) { - throw canceled(); - } + if (token && token.isCancellationRequested) { + throw canceled(); + } - query.folderQueries = folderQueries.filter((q, index) => exists[index]); - const event: Event = this.raw.textSearch(query); + const event: Event = this.raw.textSearch(query); - return DiskSearch.collectResultsFromEvent(event, onProgress, token); - }); + return DiskSearch.collectResultsFromEvent(event, onProgress, token); } fileSearch(query: IFileQuery, token?: CancellationToken): Promise { - const folderQueries = query.folderQueries || []; - return Promise.all(folderQueries.map(q => this.fileService.exists(q.folder))) - .then(exists => { - if (token && token.isCancellationRequested) { - throw canceled(); - } + if (token && token.isCancellationRequested) { + throw canceled(); + } - query.folderQueries = folderQueries.filter((q, index) => exists[index]); - let event: Event; - event = this.raw.fileSearch(query); + let event: Event; + event = this.raw.fileSearch(query); - const onProgress = (p: ISearchProgressItem) => { - if (!isFileMatch(p)) { - // Should only be for logs - this.logService.debug('SearchService#search', p.message); - } - }; + const onProgress = (p: ISearchProgressItem) => { + if (!isFileMatch(p)) { + // Should only be for logs + this.logService.debug('SearchService#search', p.message); + } + }; - return DiskSearch.collectResultsFromEvent(event, onProgress, token); - }); + return DiskSearch.collectResultsFromEvent(event, onProgress, token); } /** diff --git a/src/vs/workbench/services/search/test/node/fileSearch.integrationTest.ts b/src/vs/workbench/services/search/test/node/fileSearch.integrationTest.ts new file mode 100644 index 00000000000..427de8a4fd9 --- /dev/null +++ b/src/vs/workbench/services/search/test/node/fileSearch.integrationTest.ts @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import * as path from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import { IFileQuery, IFolderQuery, ISerializedSearchProgressItem, isProgressMessage, QueryType } from 'vs/workbench/services/search/common/search'; +import { SearchService } from 'vs/workbench/services/search/node/rawSearchService'; + +const TEST_FIXTURES = path.normalize(getPathFromAmdModule(require, './fixtures')); +const TEST_FIXTURES2 = path.normalize(getPathFromAmdModule(require, './fixtures2')); +const EXAMPLES_FIXTURES = path.join(TEST_FIXTURES, 'examples'); +const MORE_FIXTURES = path.join(TEST_FIXTURES, 'more'); +const TEST_ROOT_FOLDER: IFolderQuery = { folder: URI.file(TEST_FIXTURES) }; +const ROOT_FOLDER_QUERY: IFolderQuery[] = [ + TEST_ROOT_FOLDER +]; + +const MULTIROOT_QUERIES: IFolderQuery[] = [ + { folder: URI.file(EXAMPLES_FIXTURES), folderName: 'examples_folder' }, + { folder: URI.file(MORE_FIXTURES) } +]; + +async function doSearchTest(query: IFileQuery, expectedResultCount: number | Function): Promise { + const svc = new SearchService(); + + const results: ISerializedSearchProgressItem[] = []; + await svc.doFileSearch(query, e => { + if (!isProgressMessage(e)) { + if (Array.isArray(e)) { + results.push(...e); + } else { + results.push(e); + } + } + }); + + assert.equal(results.length, expectedResultCount, `rg ${results.length} !== ${expectedResultCount}`); +} + +suite('FileSearch-integration', function () { + this.timeout(1000 * 60); // increase timeout for this suite + + test('File - simple', () => { + const config: IFileQuery = { + type: QueryType.File, + folderQueries: ROOT_FOLDER_QUERY + }; + + return doSearchTest(config, 14); + }); + + test('File - filepattern', () => { + const config: IFileQuery = { + type: QueryType.File, + folderQueries: ROOT_FOLDER_QUERY, + filePattern: 'anotherfile' + }; + + return doSearchTest(config, 1); + }); + + test('File - exclude', () => { + const config: IFileQuery = { + type: QueryType.File, + folderQueries: ROOT_FOLDER_QUERY, + filePattern: 'file', + excludePattern: { '**/anotherfolder/**': true } + }; + + return doSearchTest(config, 2); + }); + + test('File - multiroot', () => { + const config: IFileQuery = { + type: QueryType.File, + folderQueries: MULTIROOT_QUERIES, + filePattern: 'file', + excludePattern: { '**/anotherfolder/**': true } + }; + + return doSearchTest(config, 2); + }); + + test('File - multiroot with folder name', () => { + const config: IFileQuery = { + type: QueryType.File, + folderQueries: MULTIROOT_QUERIES, + filePattern: 'examples_folder anotherfile' + }; + + return doSearchTest(config, 1); + }); + + test('File - multiroot with folder name and sibling exclude', () => { + const config: IFileQuery = { + type: QueryType.File, + folderQueries: [ + { folder: URI.file(TEST_FIXTURES), folderName: 'folder1' }, + { folder: URI.file(TEST_FIXTURES2) } + ], + filePattern: 'folder1 site', + excludePattern: { '*.css': { when: '$(basename).less' } } + }; + + return doSearchTest(config, 1); + }); +}); diff --git a/src/vs/workbench/services/search/test/node/rawSearchService.test.ts b/src/vs/workbench/services/search/test/node/rawSearchService.test.ts index b61f702fc56..1626179ccab 100644 --- a/src/vs/workbench/services/search/test/node/rawSearchService.test.ts +++ b/src/vs/workbench/services/search/test/node/rawSearchService.test.ts @@ -83,6 +83,7 @@ suite('RawSearchService', () => { const rawMatch: IRawFileMatch = { base: path.normalize('/some'), relativePath: 'where', + searchPath: undefined }; const match: ISerializedFileMatch = { @@ -232,7 +233,8 @@ suite('RawSearchService', () => { base: path.normalize('/some/where'), relativePath, basename: relativePath, - size: 3 + size: 3, + searchPath: undefined })); const Engine = TestSearchEngine.bind(null, () => matches.shift()!); const service = new RawSearchService(); @@ -291,7 +293,8 @@ suite('RawSearchService', () => { base: path.normalize('/some/where'), relativePath, basename: relativePath, - size: 3 + size: 3, + searchPath: undefined })); const Engine = TestSearchEngine.bind(null, () => matches.shift()!); const service = new RawSearchService(); @@ -340,6 +343,7 @@ suite('RawSearchService', () => { matches.push({ base: path.normalize('/some/where'), relativePath: 'bc', + searchPath: undefined }); const results: any[] = []; const cb: IProgressCallback = value => { diff --git a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts index 6e8d77a82a5..1d2ad24fb75 100644 --- a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts @@ -46,7 +46,7 @@ function doSearchTest(query: ITextQuery, expectedResultCount: number | Function) }); } -suite('Search-integration', function () { +suite('TextSearch-integration', function () { this.timeout(1000 * 60); // increase timeout for this suite test('Text: GameOfLife', () => { diff --git a/src/vs/workbench/services/statusbar/common/statusbar.ts b/src/vs/workbench/services/statusbar/common/statusbar.ts index 7a90dfab5f3..af13535d622 100644 --- a/src/vs/workbench/services/statusbar/common/statusbar.ts +++ b/src/vs/workbench/services/statusbar/common/statusbar.ts @@ -90,6 +90,11 @@ export interface IStatusbarService { */ updateEntryVisibility(id: string, visible: boolean): void; + /** + * Focused the status bar. If one of the status bar entries was focused, focuses it directly. + */ + focus(preserveEntryFocus?: boolean): void; + /** * Focuses the next status bar entry. If none focused, focuses the first. */ diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 6875ce28b08..a0dc70de8fb 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -98,6 +98,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor super(); storageKeysSyncRegistryService.registerStorageKey({ key: ViewDescriptorService.CACHED_VIEW_POSITIONS, version: 1 }); + storageKeysSyncRegistryService.registerStorageKey({ key: ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS, version: 1 }); this.viewContainerModels = new Map(); this.activeViewContextKeys = new Map>(); this.movableViewContextKeys = new Map>(); diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts index 3feced17fa1..22a0bee904a 100644 --- a/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -342,7 +342,7 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode private updateContainerInfo(): void { /* Use default container info if one of the visible view descriptors belongs to the current container by default */ const useDefaultContainerInfo = this.container.alwaysUseContainerInfo || this.visibleViewDescriptors.length === 0 || this.visibleViewDescriptors.some(v => Registry.as(ViewExtensions.ViewsRegistry).getViewContainer(v.id) === this.container); - const title = useDefaultContainerInfo ? this.container.name : this.visibleViewDescriptors[0]?.name || ''; + const title = useDefaultContainerInfo ? this.container.name : this.visibleViewDescriptors[0]?.containerTitle || this.visibleViewDescriptors[0]?.name || ''; let titleChanged: boolean = false; if (this._title !== title) { this._title = title; diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index 0b0827adb8a..7695ee2df5b 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -41,15 +41,20 @@ suite('NotebookConcatDocument', function () { rpcProtocol.set(MainContext.MainThreadNotebook, new class extends mock() { async $registerNotebookProvider() { } async $unregisterNotebookProvider() { } - async $createNotebookDocument() { } }); extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors); - let reg = extHostNotebooks.registerNotebookProvider(nullExtensionDescription, 'test', new class extends mock() { - async resolveNotebook() { } + let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock() { + // async openNotebook() { } + }); + await extHostNotebooks.$acceptDocumentAndEditorsDelta({ + addedDocuments: [{ + handle: 0, + uri: notebookUri, + viewType: 'test' + }] }); - await extHostNotebooks.$resolveNotebook('test', notebookUri); extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, versionId: 0, @@ -62,7 +67,7 @@ suite('NotebookConcatDocument', function () { outputs: [], }]]] }); - await extHostNotebooks.$updateActiveEditor('test', notebookUri); + await extHostNotebooks.$acceptDocumentAndEditorsDelta({ newActiveEditor: notebookUri }); notebook = extHostNotebooks.activeNotebookDocument!; diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts index 2f18fe411d6..8ba94daf85e 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts @@ -79,6 +79,8 @@ interface GroupEvents { closed: EditorCloseEvent[]; pinned: EditorInput[]; unpinned: EditorInput[]; + sticky: EditorInput[]; + unsticky: EditorInput[]; moved: EditorInput[]; disposed: EditorInput[]; } @@ -90,6 +92,8 @@ function groupListener(group: EditorGroup): GroupEvents { activated: [], pinned: [], unpinned: [], + sticky: [], + unsticky: [], moved: [], disposed: [] }; @@ -98,6 +102,7 @@ function groupListener(group: EditorGroup): GroupEvents { group.onDidCloseEditor(e => groupEvents.closed.push(e)); group.onDidActivateEditor(e => groupEvents.activated.push(e)); group.onDidChangeEditorPinned(e => group.isPinned(e) ? groupEvents.pinned.push(e) : groupEvents.unpinned.push(e)); + group.onDidChangeEditorSticky(e => group.isSticky(e) ? groupEvents.sticky.push(e) : groupEvents.unsticky.push(e)); group.onDidMoveEditor(e => groupEvents.moved.push(e)); group.onDidDisposeEditor(e => groupEvents.disposed.push(e)); @@ -609,6 +614,12 @@ suite('Workbench editor groups', () => { group.pin(sameInput1); assert.equal(events.pinned[0], input1); + group.stick(sameInput1); + assert.equal(events.sticky[0], input1); + + group.unstick(sameInput1); + assert.equal(events.unsticky[0], input1); + group.moveEditor(sameInput1, 1); assert.equal(events.moved[0], input1); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 436d9e6bcb0..74001f5319e 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -188,7 +188,7 @@ import 'vs/workbench/contrib/markers/browser/markers.contribution'; import 'vs/workbench/contrib/comments/browser/comments.contribution'; // URL Support -import 'vs/workbench/contrib/url/common/url.contribution'; +import 'vs/workbench/contrib/url/browser/url.contribution'; // Webview import 'vs/workbench/contrib/webview/browser/webview.contribution'; diff --git a/yarn.lock b/yarn.lock index f9925bf225d..9aac06278c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7012,10 +7012,10 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -playwright-core@=0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-0.15.0.tgz#c605c98a13c81d5a2e2691f15d04758cf302c20a" - integrity sha512-uTm4PoF2U3iXkLMMG9vlTxlGfO8atQGAHDxqi8xV7hEjNSYeLTU7c6HN5zwadeHRVuBbNsZ4yqu9u4hoqC7uxQ== +playwright-core@=1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.0.1.tgz#823b6f1afa16917ffd418f3cec0c14688a985738" + integrity sha512-a71FjUDRFqWLG3VBAojVen2TaZiXkuog+ZmI0Nh0+/QndFUbbW3kameOfUTMXFvLUGWx2ipERZx6EQTJMEQDMA== dependencies: debug "^4.1.1" extract-zip "^2.0.0" @@ -7028,12 +7028,12 @@ playwright-core@=0.15.0: rimraf "^3.0.2" ws "^6.1.0" -playwright@0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-0.15.0.tgz#bf5c3bb8404975aba78459310742388c08438431" - integrity sha512-UGHkQz8DT43uJ0KgMh2rmj8BI4FE5ReQJ9nm5mG68tt1Cj2sXPdM2b05qptfYYBPtQRetQqtJTauZ6rlCDemaQ== +playwright@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.0.1.tgz#326d479829a3505799ddc9988cc8decf5a7f8376" + integrity sha512-kVTE7uvZ7OcDVOBx7MVArUm2nbzzzpauKV9tuVIAH6vWGsOWbGGALUoTWMzNDzsPPTBJXXmxzC4KgI2zN+kVhw== dependencies: - playwright-core "=0.15.0" + playwright-core "=1.0.1" plist@^3.0.1: version "3.0.1"