diff --git a/.github/classifier.json b/.github/classifier.json index 420cce5bfaf..c9196fc5691 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vscode-github-triage-actions/master/classifier-deep/apply/apply-labels/deep-classifier-config.schema.json", + "vacation": ["joaomoreno"], "assignees": { - "joaomoreno": {"accuracy": 1.5}, "JacksonKearl": {"accuracy": 0.5} }, "labels": { @@ -72,10 +72,10 @@ "file-watcher": {"assign": ["bpasero"]}, "font-rendering": {"assign": []}, "formatting": {"assign": []}, - "git": {"assign": ["joaomoreno"], "accuracy": 1.5}, + "git": {"assign": ["joaomoreno"]}, "gpu": {"assign": ["deepak1556"]}, "grammar": {"assign": ["mjbvz"]}, - "grid-view": {"assign": ["joaomoreno"], "accuracy": 1.5}, + "grid-view": {"assign": ["joaomoreno"]}, "html": {"assign": ["aeschli"]}, "i18n": {"assign": []}, "icon-brand": {"assign": []}, @@ -86,7 +86,7 @@ "integrated-terminal-links": {"assign": ["Tyriar"]}, "integration-test": {"assign": []}, "intellisense-config": {"assign": []}, - "ipc": {"assign": ["joaomoreno"], "accuracy": 1.5}, + "ipc": {"assign": ["joaomoreno"]}, "issue-bot": {"assign": ["chrmarti"]}, "issue-reporter": {"assign": ["RMacfarlane"]}, "javascript": {"assign": ["mjbvz"]}, @@ -98,7 +98,7 @@ "languages-diagnostics": {"assign": ["jrieken"]}, "layout": {"assign": ["sbatten"]}, "lcd-text-rendering": {"assign": []}, - "list": {"assign": ["joaomoreno"], "accuracy": 1.5}, + "list": {"assign": ["joaomoreno"]}, "log": {"assign": []}, "markdown": {"assign": ["mjbvz"]}, "marketplace": {"assign": []}, @@ -111,7 +111,7 @@ "perf-bloat": {"assign": []}, "perf-startup": {"assign": []}, "php": {"assign": ["roblourens"]}, - "portable-mode": {"assign": ["joaomoreno"], "accuracy": 1.5}, + "portable-mode": {"assign": ["joaomoreno"]}, "proxy": {"assign": []}, "quick-pick": {"assign": ["chrmarti"]}, "references-viewlet": {"assign": ["jrieken"]}, @@ -119,7 +119,7 @@ "remote": {"assign": []}, "remote-explorer": {"assign": ["alexr00"]}, "rename": {"assign": ["jrieken"]}, - "scm": {"assign": ["joaomoreno"], "accuracy": 1.5}, + "scm": {"assign": ["joaomoreno"]}, "screencast-mode": {"assign": ["lszomoru"]}, "search": {"assign": ["roblourens"]}, "search-editor": {"assign": ["JacksonKearl"]}, @@ -130,9 +130,9 @@ "simple-file-dialog": {"assign": ["alexr00"]}, "smart-select": {"assign": ["jrieken"]}, "smoke-test": {"assign": []}, - "snap": {"assign": ["joaomoreno"], "accuracy": 1.5}, + "snap": {"assign": ["joaomoreno"]}, "snippets": {"assign": ["jrieken"]}, - "splitview": {"assign": ["joaomoreno"], "accuracy": 1.5}, + "splitview": {"assign": ["joaomoreno"]}, "suggest": {"assign": ["jrieken"]}, "tasks": {"assign": ["alexr00"]}, "telemetry": {"assign": []}, @@ -141,7 +141,7 @@ "timeline-git": {"assign": ["eamodio"]}, "titlebar": {"assign": ["sbatten"]}, "tokenization": {"assign": []}, - "tree": {"assign": ["joaomoreno"], "accuracy": 1.5}, + "tree": {"assign": ["joaomoreno"]}, "typescript": {"assign": ["mjbvz"]}, "undo-redo": {"assign": []}, "unit-test": {"assign": []}, diff --git a/.github/workflows/author-verified.yml b/.github/workflows/author-verified.yml index cf67a849350..c0f5efc51d4 100644 --- a/.github/workflows/author-verified.yml +++ b/.github/workflows/author-verified.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v32 + ref: v33 path: ./actions - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'author-verification-requested') diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index afdf723c4ed..63e43a17703 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -13,7 +13,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v32 + ref: v33 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Commands diff --git a/.github/workflows/deep-classifier-monitor.yml b/.github/workflows/deep-classifier-monitor.yml index 38c2c7df21d..545f74c273b 100644 --- a/.github/workflows/deep-classifier-monitor.yml +++ b/.github/workflows/deep-classifier-monitor.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v32 + ref: v33 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-runner.yml b/.github/workflows/deep-classifier-runner.yml index a2f86a9da0a..3db13a01c2b 100644 --- a/.github/workflows/deep-classifier-runner.yml +++ b/.github/workflows/deep-classifier-runner.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v32 + ref: v33 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-scraper.yml b/.github/workflows/deep-classifier-scraper.yml index 0f0de92ce0d..a78b4d14c2f 100644 --- a/.github/workflows/deep-classifier-scraper.yml +++ b/.github/workflows/deep-classifier-scraper.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v32 + ref: v33 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/english-please.yml b/.github/workflows/english-please.yml index 6728423998e..b81f12ab39f 100644 --- a/.github/workflows/english-please.yml +++ b/.github/workflows/english-please.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v32 + ref: v33 path: ./actions - name: Install Actions if: contains(github.event.issue.labels.*.name, '*english-please') diff --git a/.github/workflows/feature-request.yml b/.github/workflows/feature-request.yml index af3b36fa026..9d25f4a9f5c 100644 --- a/.github/workflows/feature-request.yml +++ b/.github/workflows/feature-request.yml @@ -18,7 +18,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v32 + ref: v33 - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') run: npm install --production --prefix ./actions diff --git a/.github/workflows/latest-release-monitor.yml b/.github/workflows/latest-release-monitor.yml index 99eabe33b33..acca00a07e3 100644 --- a/.github/workflows/latest-release-monitor.yml +++ b/.github/workflows/latest-release-monitor.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v32 + ref: v33 - name: Install Actions run: npm install --production --prefix ./actions - name: Install Storage Module diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index 7c2d8b119c6..776203c8c6c 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v32 + ref: v33 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Locker diff --git a/.github/workflows/needs-more-info-closer.yml b/.github/workflows/needs-more-info-closer.yml index fe2f76f0e62..8ecdd5fb9be 100644 --- a/.github/workflows/needs-more-info-closer.yml +++ b/.github/workflows/needs-more-info-closer.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v32 + ref: v33 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Needs More Info Closer diff --git a/.github/workflows/on-label.yml b/.github/workflows/on-label.yml index 638625f1713..732842501a6 100644 --- a/.github/workflows/on-label.yml +++ b/.github/workflows/on-label.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v32 + ref: v33 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml index c02f5a9f117..3c003d0797a 100644 --- a/.github/workflows/on-open.yml +++ b/.github/workflows/on-open.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v32 + ref: v33 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/release-pipeline-labeler.yml b/.github/workflows/release-pipeline-labeler.yml index fdabe0ddc07..8386e8f0f9f 100644 --- a/.github/workflows/release-pipeline-labeler.yml +++ b/.github/workflows/release-pipeline-labeler.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v32 + ref: v33 path: ./actions - name: Checkout Repo if: github.event_name != 'issues' diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 66beb3f1742..73da8e6b928 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v32 + ref: v33 - name: Install Actions if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') run: npm install --production --prefix ./actions diff --git a/.vscode/launch.json b/.vscode/launch.json index 577b733df80..1d966c8b508 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -226,7 +226,9 @@ "--no-cached-data", ], "webRoot": "${workspaceFolder}", - // Settings for js-debug: + "cascadeTerminateToConfigurations": [ + "Attach to Extension Host" + ], "userDataDir": false, "pauseForSourceMap": false, "outFiles": [ @@ -436,6 +438,7 @@ "Attach to Extension Host", "Attach to Shared Process", ], + "preLaunchTask": "Ensure Prelaunch Dependencies", "presentation": { "group": "0_vscode", "order": 1 diff --git a/build/azure-pipelines/publish-types/update-types.ts b/build/azure-pipelines/publish-types/update-types.ts index c2677d446c6..9603726bebf 100644 --- a/build/azure-pipelines/publish-types/update-types.ts +++ b/build/azure-pipelines/publish-types/update-types.ts @@ -45,7 +45,7 @@ function repeat(str: string, times: number): string { } function convertTabsToSpaces(str: string): string { - return str.replace(/^\t+/gm, value => repeat(' ', value.length)); + return str.replace(/\t/gm, value => repeat(' ', value.length)); } function getNewFileContent(content: string, tag: string) { diff --git a/extensions/markdown-language-features/src/test/inMemoryDocument.ts b/extensions/markdown-language-features/src/test/inMemoryDocument.ts index c2472e5a4ec..052216f90f5 100644 --- a/extensions/markdown-language-features/src/test/inMemoryDocument.ts +++ b/extensions/markdown-language-features/src/test/inMemoryDocument.ts @@ -22,6 +22,7 @@ export class InMemoryDocument implements vscode.TextDocument { isDirty: boolean = false; isClosed: boolean = false; eol: vscode.EndOfLine = vscode.EndOfLine.LF; + notebook: undefined; get fileName(): string { return this.uri.fsPath; @@ -66,4 +67,4 @@ export class InMemoryDocument implements vscode.TextDocument { save(): never { throw new Error('Method not implemented.'); } -} \ No newline at end of file +} diff --git a/extensions/package.json b/extensions/package.json index 5577dde9099..3307d7709cb 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": "^4.0.1-rc" + "typescript": "^4.0.1-insiders.20200813" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 8b2f8c057e0..8e2afa3ba13 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, Pseudoterminal, EventEmitter, TerminalDimensions, workspace, ConfigurationTarget, Disposable, UIKind, env, EnvironmentVariableMutatorType, EnvironmentVariableMutator, extensions, ExtensionContext } from 'vscode'; +import { window, Pseudoterminal, EventEmitter, TerminalDimensions, workspace, ConfigurationTarget, Disposable, UIKind, env, EnvironmentVariableMutatorType, EnvironmentVariableMutator, extensions, ExtensionContext, TerminalOptions, ExtensionTerminalOptions } from 'vscode'; import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert'; // Disable terminal tests: @@ -168,8 +168,10 @@ import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert'; const terminal = window.createTerminal(options); try { equal(terminal.name, 'foo'); - deepEqual(terminal.creationOptions, options); - throws(() => (terminal.creationOptions).name = 'bad', 'creationOptions should be readonly at runtime'); + const terminalOptions = terminal.creationOptions as TerminalOptions; + equal(terminalOptions.name, 'foo'); + equal(terminalOptions.hideFromUser, true); + throws(() => terminalOptions.name = 'bad', 'creationOptions should be readonly at runtime'); } catch (e) { done(e); return; @@ -460,10 +462,6 @@ import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert'; } term.show(); disposables.push(window.onDidChangeTerminalDimensions(e => { - if (e.dimensions.columns === 0 || e.dimensions.rows === 0) { - // HACK: Ignore the event if dimension(s) are zero (#83778) - return; - } // The default pty dimensions have a chance to appear here since override // dimensions happens after the terminal is created. If so just ignore and // wait for the right dimensions @@ -609,8 +607,10 @@ import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert'; const terminal = window.createTerminal(options); try { equal(terminal.name, 'foo'); - deepEqual(terminal.creationOptions, options); - throws(() => (terminal.creationOptions).name = 'bad', 'creationOptions should be readonly at runtime'); + const terminalOptions = terminal.creationOptions as ExtensionTerminalOptions; + equal(terminalOptions.name, 'foo'); + equal(terminalOptions.pty, pty); + throws(() => terminalOptions.name = 'bad', 'creationOptions should be readonly at runtime'); } catch (e) { done(e); } diff --git a/extensions/vscode-web-playground/package.json b/extensions/vscode-web-playground/package.json index 954aec0fae9..67ebd70edd4 100644 --- a/extensions/vscode-web-playground/package.json +++ b/extensions/vscode-web-playground/package.json @@ -8,7 +8,6 @@ "private": true, "activationEvents": [ "onFileSystem:memfs", - "onFileSystem:github", "onDebug" ], "browser": "./dist/browser/extension", diff --git a/extensions/vscode-web-playground/src/memfs.ts b/extensions/vscode-web-playground/src/memfs.ts index 969b2c4b6fa..76c6f3f0e2e 100644 --- a/extensions/vscode-web-playground/src/memfs.ts +++ b/extensions/vscode-web-playground/src/memfs.ts @@ -107,7 +107,7 @@ export class MemFS implements FileSystemProvider, FileSearchProvider, TextSearch this.writeFile(Uri.parse(`memfs:/sample-folder/file.py`), textEncoder.encode('import base64, sys; base64.decode(open(sys.argv[1], "rb"), open(sys.argv[2], "wb"))'), { create: true, overwrite: true }); this.writeFile(Uri.parse(`memfs:/sample-folder/file.yaml`), textEncoder.encode('- just: write something'), { create: true, overwrite: true }); this.writeFile(Uri.parse(`memfs:/sample-folder/file.jpg`), getImageFile(), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.php`), textEncoder.encode('&1\'); ?>'), { create: true, overwrite: true }); + this.writeFile(Uri.parse(`memfs:/sample-folder/file.php`), textEncoder.encode(''), { create: true, overwrite: true }); // some more files & folders this.createDirectory(Uri.parse(`memfs:/sample-folder/folder/`)); diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 9e459157c1b..9a140984799 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@^4.0.1-rc: - version "4.0.1-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.1-rc.tgz#8adc78223eae56fe71d906a5fa90c3543b07a677" - integrity sha512-TCkspT3dSKOykbzS3/WSK7pqU2h1d/lEO6i45Afm5Y3XNAEAo8YXTG3kHOQk/wFq/5uPyO1+X8rb/Q+g7UsxJw== +typescript@^4.0.1-insiders.20200813: + version "4.0.1-insiders.20200813" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.1-insiders.20200813.tgz#0b17335a7517023be0f1ce947052662ab2bde1f0" + integrity sha512-mTQPs9uyxv6jLEO5Z+LJpFUSQwx9KI3ZD+2Uv9e5O32Oz/16snCB5skBHw5k1PchsXOZCG6xcB902qmgjI0tWQ== diff --git a/src/main.js b/src/main.js index 9a49d06217a..9d68c984c66 100644 --- a/src/main.js +++ b/src/main.js @@ -313,17 +313,6 @@ function createDefaultArgvConfigSync(argvConfigPath) { fs.mkdirSync(argvConfigPathDirname); } - // Migrate over legacy locale - const localeConfigPath = path.join(userDataPath, 'User', 'locale.json'); - const legacyLocale = getLegacyUserDefinedLocaleSync(localeConfigPath); - if (legacyLocale) { - try { - fs.unlinkSync(localeConfigPath); - } catch (error) { - //ignore - } - } - // Default argv content const defaultArgvConfigContent = [ '// This configuration file allows you to pass permanent command line arguments to VS Code.', @@ -340,19 +329,10 @@ function createDefaultArgvConfigSync(argvConfigPath) { '', ' // Enabled by default by VS Code to resolve color issues in the renderer', ' // See https://github.com/Microsoft/vscode/issues/51791 for details', - ' "disable-color-correct-rendering": true' + ' "disable-color-correct-rendering": true', + '}' ]; - if (legacyLocale) { - defaultArgvConfigContent[defaultArgvConfigContent.length - 1] = `${defaultArgvConfigContent[defaultArgvConfigContent.length - 1]},`; // append trailing "," - - defaultArgvConfigContent.push(''); - defaultArgvConfigContent.push(' // Display language of VS Code'); - defaultArgvConfigContent.push(` "locale": "${legacyLocale}"`); - } - - defaultArgvConfigContent.push('}'); - // Create initial argv.json with default content fs.writeFileSync(argvConfigPath, defaultArgvConfigContent.join('\n')); } catch (error) { @@ -610,19 +590,4 @@ function getUserDefinedLocale(argvConfig) { return argvConfig.locale && typeof argvConfig.locale === 'string' ? argvConfig.locale.toLowerCase() : undefined; } -/** - * @param {string} localeConfigPath - * @returns {string | undefined} - */ -function getLegacyUserDefinedLocaleSync(localeConfigPath) { - try { - const content = stripComments(fs.readFileSync(localeConfigPath).toString()); - - const value = JSON.parse(content).locale; - return value && typeof value === 'string' ? value.toLowerCase() : undefined; - } catch (error) { - // ignore - } -} - //#endregion diff --git a/src/vs/base/common/comparers.ts b/src/vs/base/common/comparers.ts index a25e2bb9c16..7b45ce5abb6 100644 --- a/src/vs/base/common/comparers.ts +++ b/src/vs/base/common/comparers.ts @@ -33,8 +33,7 @@ const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Coll return { collator: collator }; -}); - +});/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */ export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number { const a = one || ''; const b = other || ''; @@ -49,36 +48,16 @@ export function compareFileNames(one: string | null, other: string | null, caseS 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); +/** Compares filenames without distinguishing the name from the extension. Disambiguates by length, not unicode comparison. */ +export function compareFileNamesDefault(one: string | null, other: string | null): number { const collatorNumeric = intlFileNameCollatorNumeric.value.collator; - const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; - let result; + one = one || ''; + other = other || ''; - // 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; + // Compare the entire filename - both name and extension - and disambiguate by length if needed + return compareAndDisambiguateByLength(collatorNumeric, one, other); } -const FileNameMatch = /^(.*?)(\.([^.]*))?$/; - export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number { if (!caseSensitive) { one = one && one.toLowerCase(); @@ -123,10 +102,12 @@ export function compareFileExtensions(one: string | null, other: string | null): return result; } -/** 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); +/** Compares filenames by extenson, then by full filename */ +export function compareFileExtensionsDefault(one: string | null, other: string | null): number { + one = one || ''; + other = other || ''; + const oneExtension = extractExtension(one); + const otherExtension = extractExtension(other); const collatorNumeric = intlFileNameCollatorNumeric.value.collator; const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; let result; @@ -137,20 +118,12 @@ export function compareFileExtensionsNumeric(one: string | null, other: string | 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; + // Compare full filenames + return compareAndDisambiguateByLength(collatorNumeric, one, other); } +const FileNameMatch = /^(.*?)(\.([^.]*))?$/; + /** 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); @@ -166,6 +139,13 @@ function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): return result; } +/** Extracts the extension from a full filename. Treats dotfiles as names, not extensions. */ +function extractExtension(str?: string | null): string { + const match = str ? FileNameMatch.exec(str) as Array : ([] as Array); + + return (match && match[1] && match[1].charAt(0) !== '.' && match[3]) || ''; +} + function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) { // Check for differences let result = collator.compare(one, other); diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index 9a4f7bc51bc..1c8e5ef5fb5 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { compareAnything } from 'vs/base/common/comparers'; -import { matchesPrefix, IMatch, matchesCamelCase, isUpper, fuzzyScore, createMatches as createFuzzyMatches, matchesStrictPrefix } from 'vs/base/common/filters'; +import { IMatch, isUpper, fuzzyScore, createMatches as createFuzzyMatches } from 'vs/base/common/filters'; import { sep } from 'vs/base/common/path'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { stripWildcards, equalsIgnoreCase } from 'vs/base/common/strings'; @@ -168,7 +168,7 @@ function computeCharScore(queryCharAtIndex: string, queryLowerCharAtIndex: strin score += 1; // if (DEBUG) { - // console.groupCollapsed(`%cCharacter match bonus: +1 (char: ${queryLower[queryIndex]} at index ${targetIndex}, total score: ${score})`, 'font-weight: normal'); + // console.groupCollapsed(`%cCharacter match bonus: +1 (char: ${queryLowerCharAtIndex} at index ${targetIndex}, total score: ${score})`, 'font-weight: normal'); // } // Consecutive match bonus @@ -176,7 +176,7 @@ function computeCharScore(queryCharAtIndex: string, queryLowerCharAtIndex: strin score += (matchesSequenceLength * 5); // if (DEBUG) { - // console.log('Consecutive match bonus: ' + (matchesSequenceLength * 5)); + // console.log(`Consecutive match bonus: +${matchesSequenceLength * 5}`); // } } @@ -206,16 +206,16 @@ function computeCharScore(queryCharAtIndex: string, queryLowerCharAtIndex: strin score += separatorBonus; // if (DEBUG) { - // console.log('After separtor bonus: +4'); + // console.log(`After separtor bonus: +${separatorBonus}`); // } } // Inside word upper case bonus (camel case) else if (isUpper(target.charCodeAt(targetIndex))) { - score += 1; + score += 2; // if (DEBUG) { - // console.log('Inside word upper case bonus: +1'); + // console.log('Inside word upper case bonus: +2'); // } } } @@ -369,10 +369,7 @@ export interface IItemAccessor { } const PATH_IDENTITY_SCORE = 1 << 18; -const LABEL_PREFIX_SCORE_MATCHCASE = 1 << 17; -const LABEL_PREFIX_SCORE_IGNORECASE = 1 << 16; -const LABEL_CAMELCASE_SCORE = 1 << 15; -const LABEL_SCORE_THRESHOLD = 1 << 14; +const LABEL_SCORE_THRESHOLD = 1 << 17; export function scoreItemFuzzy(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor, cache: FuzzyScorerCache): IItemScore { if (!item || !query.normalized) { @@ -386,11 +383,17 @@ export function scoreItemFuzzy(item: T, query: IPreparedQuery, fuzzy: boolean const description = accessor.getItemDescription(item); + // in order to speed up scoring, we cache the score with a unique hash based on: + // - label + // - description (if provided) + // - query (normalized) + // - number of query pieces (i.e. 'hello world' and 'helloworld' are different) + // - wether fuzzy matching is enabled or not let cacheHash: string; if (description) { - cacheHash = `${label}${description}${query.normalized}${fuzzy}`; + cacheHash = `${label}${description}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}`; } else { - cacheHash = `${label}${query.normalized}${fuzzy}`; + cacheHash = `${label}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}`; } const cached = cache[cacheHash]; @@ -457,21 +460,6 @@ function doScoreItemFuzzySingle(label: string, description: string | undefined, // Prefer label matches if told so if (preferLabelMatches) { - - // Treat prefix matches on the label highest - const prefixLabelMatchIgnoreCase = matchesPrefix(query.normalized, label); - if (prefixLabelMatchIgnoreCase) { - const prefixLabelMatchStrictCase = matchesStrictPrefix(query.normalized, label); - return { score: prefixLabelMatchStrictCase ? LABEL_PREFIX_SCORE_MATCHCASE : LABEL_PREFIX_SCORE_IGNORECASE, labelMatch: prefixLabelMatchStrictCase || prefixLabelMatchIgnoreCase }; - } - - // Treat camelcase matches on the label second highest - const camelcaseLabelMatch = matchesCamelCase(query.normalized, label); - if (camelcaseLabelMatch) { - return { score: LABEL_CAMELCASE_SCORE, labelMatch: camelcaseLabelMatch }; - } - - // Prefer scores on the label if any const [labelScore, labelPositions] = scoreFuzzy(label, query.normalized, query.normalizedLowercase, fuzzy); if (labelScore) { return { score: labelScore + LABEL_SCORE_THRESHOLD, labelMatch: createMatches(labelPositions) }; @@ -594,81 +582,39 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared const scoreA = itemScoreA.score; const scoreB = itemScoreB.score; - // 1.) prefer identity matches + // 1.) identity matches have highest score if (scoreA === PATH_IDENTITY_SCORE || scoreB === PATH_IDENTITY_SCORE) { if (scoreA !== scoreB) { return scoreA === PATH_IDENTITY_SCORE ? -1 : 1; } } - // 2.) prefer label prefix matches (match case) - if (scoreA === LABEL_PREFIX_SCORE_MATCHCASE || scoreB === LABEL_PREFIX_SCORE_MATCHCASE) { + // 2.) matches on label are considered higher compared to label+description matches + if (scoreA > LABEL_SCORE_THRESHOLD || scoreB > LABEL_SCORE_THRESHOLD) { if (scoreA !== scoreB) { - return scoreA === LABEL_PREFIX_SCORE_MATCHCASE ? -1 : 1; + return scoreA > scoreB ? -1 : 1; } - const labelA = accessor.getItemLabel(itemA) || ''; - const labelB = accessor.getItemLabel(itemB) || ''; - - // prefer shorter names when both match on label prefix - if (labelA.length !== labelB.length) { - return labelA.length - labelB.length; - } - } - - // 3.) prefer label prefix matches (ignore case) - if (scoreA === LABEL_PREFIX_SCORE_IGNORECASE || scoreB === LABEL_PREFIX_SCORE_IGNORECASE) { - if (scoreA !== scoreB) { - return scoreA === LABEL_PREFIX_SCORE_IGNORECASE ? -1 : 1; - } - - const labelA = accessor.getItemLabel(itemA) || ''; - const labelB = accessor.getItemLabel(itemB) || ''; - - // prefer shorter names when both match on label prefix - if (labelA.length !== labelB.length) { - return labelA.length - labelB.length; - } - } - - // 4.) prefer camelcase matches - if (scoreA === LABEL_CAMELCASE_SCORE || scoreB === LABEL_CAMELCASE_SCORE) { - if (scoreA !== scoreB) { - return scoreA === LABEL_CAMELCASE_SCORE ? -1 : 1; - } - - const labelA = accessor.getItemLabel(itemA) || ''; - const labelB = accessor.getItemLabel(itemB) || ''; - - // prefer more compact camel case matches over longer + // prefer more compact matches over longer in label const comparedByMatchLength = compareByMatchLength(itemScoreA.labelMatch, itemScoreB.labelMatch); if (comparedByMatchLength !== 0) { return comparedByMatchLength; } - // prefer shorter names when both match on label camelcase + // prefer shorter labels over longer labels + const labelA = accessor.getItemLabel(itemA) || ''; + const labelB = accessor.getItemLabel(itemB) || ''; if (labelA.length !== labelB.length) { return labelA.length - labelB.length; } } - // 5.) prefer label scores - if (scoreA > LABEL_SCORE_THRESHOLD || scoreB > LABEL_SCORE_THRESHOLD) { - if (scoreB < LABEL_SCORE_THRESHOLD) { - return -1; - } - - if (scoreA < LABEL_SCORE_THRESHOLD) { - return 1; - } - } - - // 6.) compare by score + // 3.) compare by score in label+description if (scoreA !== scoreB) { return scoreA > scoreB ? -1 : 1; } - // 7.) prefer matches in label over non-label matches + // 4.) scores are identical: prefer matches in label over non-label matches const itemAHasLabelMatches = Array.isArray(itemScoreA.labelMatch) && itemScoreA.labelMatch.length > 0; const itemBHasLabelMatches = Array.isArray(itemScoreB.labelMatch) && itemScoreB.labelMatch.length > 0; if (itemAHasLabelMatches && !itemBHasLabelMatches) { @@ -677,15 +623,14 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared return 1; } - // 8.) scores are identical, prefer more compact matches (label and description) + // 5.) scores are identical: prefer more compact matches (label and description) const itemAMatchDistance = computeLabelAndDescriptionMatchDistance(itemA, itemScoreA, accessor); const itemBMatchDistance = computeLabelAndDescriptionMatchDistance(itemB, itemScoreB, accessor); if (itemAMatchDistance && itemBMatchDistance && itemAMatchDistance !== itemBMatchDistance) { return itemBMatchDistance > itemAMatchDistance ? -1 : 1; } - // 9.) at this point, scores are identical and match compactness as well - // for both items so we start to use the fallback compare + // 6.) scores are identical: start to use the fallback compare return fallbackCompare(itemA, itemB, query, accessor); } diff --git a/src/vs/base/test/browser/comparers.test.ts b/src/vs/base/test/browser/comparers.test.ts index 90dff8c2835..4c40ea2a065 100644 --- a/src/vs/base/test/browser/comparers.test.ts +++ b/src/vs/base/test/browser/comparers.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareFileNames, compareFileExtensions, compareFileNamesNumeric, compareFileExtensionsNumeric } from 'vs/base/common/comparers'; +import { compareFileNames, compareFileExtensions, compareFileNamesDefault, compareFileExtensionsDefault } from 'vs/base/common/comparers'; import * as assert from 'assert'; const compareLocale = (a: string, b: string) => a.localeCompare(b); @@ -15,7 +15,7 @@ suite('Comparers', () => { test('compareFileNames', () => { // - // Comparisons with the same results as compareFileNamesNumeric + // Comparisons with the same results as compareFileNamesDefault // // name-only comparisons @@ -28,6 +28,7 @@ suite('Comparers', () => { // name plus extension comparisons assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename'); + assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale'); // dotfile comparisons assert(compareFileNames('.abc', '.abc') === 0, 'equal dotfile names should be equal'); @@ -52,7 +53,7 @@ suite('Comparers', () => { 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 + // Comparisons with different results than compareFileNamesDefault // // name-only comparisons @@ -61,9 +62,6 @@ suite('Comparers', () => { 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'); @@ -75,7 +73,7 @@ suite('Comparers', () => { test('compareFileExtensions', () => { // - // Comparisons with the same results as compareFileExtensionsNumeric + // Comparisons with the same results as compareFileExtensionsDefault // // name-only comparisons @@ -118,12 +116,8 @@ suite('Comparers', () => { assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); 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 + // Comparisons with different results from compareFileExtensionsDefault // // name-only comparisions @@ -135,6 +129,7 @@ suite('Comparers', () => { // 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'); + assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); // dotfile comparisons assert(compareFileExtensions('.env', '.aaa.env') < 0, 'a dotfile with an extension is treated as a name plus an extension - equal extensions'); @@ -152,145 +147,139 @@ suite('Comparers', () => { }); - test('compareFileNamesNumeric', () => { + test('compareFileNamesDefault', () => { // // 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'); + assert(compareFileNamesDefault(null, null) === 0, 'null should be equal'); + assert(compareFileNamesDefault(null, 'abc') < 0, 'null should be come before real values'); + assert(compareFileNamesDefault('', '') === 0, 'empty should be equal'); + assert(compareFileNamesDefault('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesDefault('z', 'A') > 0, 'z comes is after A regardless of case'); + assert(compareFileNamesDefault('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'); + assert(compareFileNamesDefault('file.ext', 'file.ext') === 0, 'equal full names should be equal'); + assert(compareFileNamesDefault('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileNamesDefault('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileNamesDefault('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + assert(compareFileNamesDefault('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole filename in locale order'); // 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'); + assert(compareFileNamesDefault('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNamesDefault('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNamesDefault('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesDefault('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesDefault('.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'); + assert(compareFileNamesDefault(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNamesDefault('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesDefault('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesDefault('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesDefault('.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'); + assert(compareFileNamesDefault('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileNamesDefault('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileNamesDefault('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileNamesDefault('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileNamesDefault('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNamesDefault('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'); + assert(compareFileNamesDefault('a', 'A') === compareLocale('a', 'A'), 'the same letter sorts by locale'); + assert(compareFileNamesDefault('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter sorts by locale'); + assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesDefault), ['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(compareFileNamesDefault), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order'); // 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'); + assert(compareFileNamesDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); + assert(compareFileNamesDefault('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); + assert(compareFileNamesDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); }); - test('compareFileExtensionsNumeric', () => { + test('compareFileExtensionsDefault', () => { // // 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'); + assert(compareFileExtensionsDefault(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsDefault(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsDefault('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsDefault('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsDefault('z', 'A') > 0, 'z comes after A'); + assert(compareFileExtensionsDefault('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'); + assert(compareFileExtensionsDefault('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsDefault('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsDefault('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileExtensionsDefault('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsDefault('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); + assert(compareFileExtensionsDefault('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'); + assert(compareFileExtensionsDefault('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsDefault('.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'); + assert(compareFileExtensionsDefault(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsDefault('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsDefault('.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'); + assert(compareFileExtensionsDefault('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileExtensionsDefault('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileExtensionsDefault('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsDefault('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); + assert(compareFileExtensionsDefault('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensionsDefault('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileExtensionsDefault('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); + assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsDefault('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileExtensionsDefault('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); + assert(compareFileExtensionsDefault('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); // // 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'); + assert(compareFileExtensionsDefault('a', 'A') === compareLocale('a', 'A'), 'the same letter of different case sorts by locale'); + assert(compareFileExtensionsDefault('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter of different case sorts by locale'); + assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsDefault), ['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(compareFileExtensionsDefault), ['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'); + assert(compareFileExtensionsDefault('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); + assert(compareFileExtensionsDefault('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale'); + assert(compareFileExtensionsDefault('aggregate.go', 'aggregate_repo.go') > 0, 'names with the same extension sort in full filename locale order'); // 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'); + assert(compareFileExtensionsDefault('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsDefault('.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'); + assert(compareFileExtensionsDefault('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsDefault('.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'); + assert(compareFileExtensionsDefault('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); + assert(compareFileExtensionsDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); + assert(compareFileExtensionsDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); + assert(compareFileExtensionsDefault('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); }); }); diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 569becfed7b..2064ec5a075 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -110,10 +110,10 @@ suite('Fuzzy Scorer', () => { scores.push(_doScore(target, 'hw', true)); // direct mix-case prefix (multiple) scores.push(_doScore(target, 'H', true)); // direct case prefix scores.push(_doScore(target, 'h', true)); // direct mix-case prefix - scores.push(_doScore(target, 'ld', true)); // in-string mix-case match (consecutive, avoids scattered hit) scores.push(_doScore(target, 'W', true)); // direct case word prefix - scores.push(_doScore(target, 'w', true)); // direct mix-case word prefix scores.push(_doScore(target, 'Ld', true)); // in-string case match (multiple) + scores.push(_doScore(target, 'ld', true)); // in-string mix-case match (consecutive, avoids scattered hit) + scores.push(_doScore(target, 'w', true)); // direct mix-case word prefix scores.push(_doScore(target, 'L', true)); // in-string case match scores.push(_doScore(target, 'l', true)); // in-string mix-case match scores.push(_doScore(target, '4', true)); // no match @@ -123,13 +123,13 @@ suite('Fuzzy Scorer', () => { assert.deepEqual(scores, sortedScores); // Assert scoring positions - let positions = scores[0][1]; - assert.equal(positions.length, 'HelLo-World'.length); + // let positions = scores[0][1]; + // assert.equal(positions.length, 'HelLo-World'.length); - positions = scores[2][1]; - assert.equal(positions.length, 'HW'.length); - assert.equal(positions[0], 0); - assert.equal(positions[1], 6); + // positions = scores[2][1]; + // assert.equal(positions.length, 'HW'.length); + // assert.equal(positions[0], 0); + // assert.equal(positions[1], 6); }); test('score (non fuzzy)', function () { @@ -626,6 +626,21 @@ suite('Fuzzy Scorer', () => { assert.equal(res[1], resourceA); }); + test('compareFilesByScore - prefer camel case matches', function () { + const resourceA = URI.file('config/test/NullPointerException.java'); + const resourceB = URI.file('config/test/nopointerexception.java'); + + for (const query of ['npe', 'NPE']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + } + }); + test('compareFilesByScore - prefer more compact camel case matches', function () { const resourceA = URI.file('config/test/openthisAnythingHandler.js'); const resourceB = URI.file('config/test/openthisisnotsorelevantforthequeryAnyHand.js'); @@ -925,6 +940,51 @@ suite('Fuzzy Scorer', () => { assert.equal(res[0], resourceB); }); + test('compareFilesByScore - prefer shorter match (bug #103052) - foo bar', function () { + const resourceA = URI.file('app/emails/foo.bar.js'); + const resourceB = URI.file('app/emails/other-footer.other-bar.js'); + + for (const query of ['foo bar', 'foobar']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + } + }); + + test('compareFilesByScore - prefer shorter match (bug #103052) - payment model', function () { + const resourceA = URI.file('app/components/payment/payment.model.js'); + const resourceB = URI.file('app/components/online-payments-history/online-payments-history.model.js'); + + for (const query of ['payment model', 'paymentmodel']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + } + }); + + test('compareFilesByScore - prefer shorter match (bug #103052) - color', function () { + const resourceA = URI.file('app/constants/color.js'); + const resourceB = URI.file('app/components/model/input/pick-avatar-color.js'); + + for (const query of ['color js', 'colorjs']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + } + }); + test('prepareQuery', () => { assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa'); assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts'); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index b69c69a1c2d..3f9eef1b816 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -1055,3 +1055,14 @@ export function getCodeEditor(thing: any): ICodeEditor | null { return null; } + +/** + *@internal + */ +export function getIEditor(thing: any): editorCommon.IEditor | null { + if (isCodeEditor(thing) || isDiffEditor(thing)) { + return thing; + } + + return null; +} diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 6989f44cb12..14e21796a7c 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -95,7 +95,7 @@ export class CodeLensContribution implements IEditorContribution { .monaco-editor .codelens-decoration.${this._styleClassName} { height: ${height}px; line-height: ${lineHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontInfo.fontSize * 0.45)}px;} .monaco-editor .codelens-decoration.${this._styleClassName} > a > .codicon { line-height: ${lineHeight}px; font-size: ${fontSize}px; } `; - this._styleElement.innerHTML = newStyle; + this._styleElement.textContent = newStyle; } private _localDispose(): void { @@ -470,5 +470,3 @@ registerEditorAction(class ShowLensesInCurrentLine extends EditorAction { } } }); - - diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 01a7dc55aa0..f5b6ec41d4f 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -167,7 +167,7 @@ class MessageWidget { let relatedResource = document.createElement('a'); dom.addClass(relatedResource, 'filename'); - relatedResource.innerHTML = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `; + relatedResource.innerText = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `; relatedResource.title = getPathLabel(related.resource, undefined); this._relatedDiagnostics.set(relatedResource, related); diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 7d429358a35..86e708fbdc8 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -429,7 +429,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { if (this._model.isEmpty) { this.setTitle(''); - this._messageContainer.innerHTML = nls.localize('noResults', "No results"); + this._messageContainer.innerText = nls.localize('noResults', "No results"); dom.show(this._messageContainer); return Promise.resolve(undefined); } diff --git a/src/vs/editor/contrib/peekView/peekView.ts b/src/vs/editor/contrib/peekView/peekView.ts index cbada7a1e48..f265a4bfd60 100644 --- a/src/vs/editor/contrib/peekView/peekView.ts +++ b/src/vs/editor/contrib/peekView/peekView.ts @@ -11,7 +11,6 @@ import { Action } from 'vs/base/common/actions'; import { Color } from 'vs/base/common/color'; import { Emitter } from 'vs/base/common/event'; import * as objects from 'vs/base/common/objects'; -import * as strings from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; @@ -223,10 +222,10 @@ export abstract class PeekViewWidget extends ZoneWidget { setTitle(primaryHeading: string, secondaryHeading?: string): void { if (this._primaryHeading && this._secondaryHeading) { - this._primaryHeading.innerHTML = strings.escape(primaryHeading); + this._primaryHeading.innerText = primaryHeading; this._primaryHeading.setAttribute('aria-label', primaryHeading); if (secondaryHeading) { - this._secondaryHeading.innerHTML = strings.escape(secondaryHeading); + this._secondaryHeading.innerText = secondaryHeading; } else { dom.clearNode(this._secondaryHeading); } @@ -236,7 +235,7 @@ export abstract class PeekViewWidget extends ZoneWidget { setMetaTitle(value: string): void { if (this._metaHeading) { if (value) { - this._metaHeading.innerHTML = strings.escape(value); + this._metaHeading.innerText = value; dom.show(this._metaHeading); } else { dom.hide(this._metaHeading); diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index 7373a0db710..360ce3a79d1 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -35,15 +35,12 @@ import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKe import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { mock } from 'vs/base/test/common/mock'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; function createMockEditor(model: TextModel): ITestCodeEditor { let editor = createTestCodeEditor({ model: model, serviceCollection: new ServiceCollection( - [IConfigurationService, TestConfigurationService], [ITelemetryService, NullTelemetryService], [IStorageService, new InMemoryStorageService()], [IKeybindingService, new MockKeybindingService()], diff --git a/src/vs/editor/contrib/suggest/test/wordDistance.test.ts b/src/vs/editor/contrib/suggest/test/wordDistance.test.ts index 7f1acc79616..05ccef17d53 100644 --- a/src/vs/editor/contrib/suggest/test/wordDistance.test.ts +++ b/src/vs/editor/contrib/suggest/test/wordDistance.test.ts @@ -81,11 +81,16 @@ suite('suggest, word distance', function () { distance = await WordDistance.create(service, editor); + disposables.add(service); disposables.add(mode); disposables.add(model); disposables.add(editor); }); + teardown(function () { + disposables.clear(); + }); + function createSuggestItem(label: string, overwriteBefore: number, position: IPosition): CompletionItem { const suggestion: modes.CompletionItem = { label, diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 4617ed161c1..687cefd9ef1 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -122,6 +122,7 @@ export class MenuId { static readonly NotebookCellTitle = new MenuId('NotebookCellTitle'); static readonly NotebookCellInsert = new MenuId('NotebookCellInsert'); static readonly NotebookCellBetween = new MenuId('NotebookCellBetween'); + static readonly NotebookCellListTop = new MenuId('NotebookCellTop'); static readonly BulkEditTitle = new MenuId('BulkEditTitle'); static readonly BulkEditContext = new MenuId('BulkEditContext'); static readonly TimelineItemContext = new MenuId('TimelineItemContext'); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index c261d8647b8..c753ef72f24 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2024,4 +2024,17 @@ declare module 'vscode' { } //#endregion + + + //#region https://github.com/microsoft/vscode/issues/102091 + + export interface TextDocument { + + /** + * The [notebook](#NotebookDocument) that contains this document as a notebook cell or `undefined` when + * the document is not contained by a notebook (this should be the more frequent case). + */ + notebook: NotebookDocument | undefined; + } + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index d9a61aa4f42..7fdf21501ff 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -415,7 +415,13 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } async $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: UriComponents[]): Promise { - const renderer = new MainThreadNotebookRenderer(this._proxy, type, extension.id, URI.revive(extension.location), selectors, preloads.map(uri => URI.revive(uri))); + const staticContribution = this._notebookService.getContributedNotebookOutputRenderers(type); + + if (!staticContribution) { + throw new Error(`Notebook renderer for '${type}' is not statically registered.`); + } + + const renderer = new MainThreadNotebookRenderer(this._proxy, type, staticContribution.displayName, extension.id, URI.revive(extension.location), selectors, preloads.map(uri => URI.revive(uri))); this._notebookRenderers.set(type, renderer); this._notebookService.registerNotebookRenderer(type, renderer); } @@ -674,10 +680,11 @@ export class MainThreadNotebookRenderer implements INotebookRendererInfo { constructor( private readonly _proxy: ExtHostNotebookShape, readonly id: string, + public displayName: string, readonly extensionId: ExtensionIdentifier, readonly extensionLocation: URI, readonly selectors: INotebookMimeTypeSelector, - readonly preloads: URI[] + readonly preloads: URI[], ) { } diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index d518ec92f6e..b64f47fad6d 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -154,6 +154,10 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._dataEventTracker = this._instantiationService.createInstance(TerminalDataEventTracker, (id, data) => { this._onTerminalData(id, data); }); + // Send initial events if they exist + this._terminalService.terminalInstances.forEach(t => { + t.initialDataEvents?.forEach(d => this._onTerminalData(t.id, d)); + }); } } diff --git a/src/vs/workbench/api/common/extHostDocumentData.ts b/src/vs/workbench/api/common/extHostDocumentData.ts index 756f1a03f84..0cd5c8378fc 100644 --- a/src/vs/workbench/api/common/extHostDocumentData.ts +++ b/src/vs/workbench/api/common/extHostDocumentData.ts @@ -29,19 +29,17 @@ export function getWordDefinitionFor(modeId: string): RegExp | undefined { export class ExtHostDocumentData extends MirrorTextModel { - private _proxy: MainThreadDocumentsShape; - private _languageId: string; - private _isDirty: boolean; private _document?: vscode.TextDocument; private _isDisposed: boolean = false; - constructor(proxy: MainThreadDocumentsShape, uri: URI, lines: string[], eol: string, - languageId: string, versionId: number, isDirty: boolean + constructor( + private readonly _proxy: MainThreadDocumentsShape, + uri: URI, lines: string[], eol: string, versionId: number, + private _languageId: string, + private _isDirty: boolean, + private readonly _notebook?: vscode.NotebookDocument | undefined ) { super(uri, lines, eol, versionId); - this._proxy = proxy; - this._languageId = languageId; - this._isDirty = isDirty; } dispose(): void { @@ -59,25 +57,26 @@ export class ExtHostDocumentData extends MirrorTextModel { get document(): vscode.TextDocument { if (!this._document) { - const data = this; + const that = this; this._document = { - get uri() { return data._uri; }, - get fileName() { return data._uri.fsPath; }, - get isUntitled() { return data._uri.scheme === Schemas.untitled; }, - get languageId() { return data._languageId; }, - get version() { return data._versionId; }, - get isClosed() { return data._isDisposed; }, - get isDirty() { return data._isDirty; }, - save() { return data._save(); }, - getText(range?) { return range ? data._getTextInRange(range) : data.getText(); }, - get eol() { return data._eol === '\n' ? EndOfLine.LF : EndOfLine.CRLF; }, - get lineCount() { return data._lines.length; }, - lineAt(lineOrPos: number | vscode.Position) { return data._lineAt(lineOrPos); }, - offsetAt(pos) { return data._offsetAt(pos); }, - positionAt(offset) { return data._positionAt(offset); }, - validateRange(ran) { return data._validateRange(ran); }, - validatePosition(pos) { return data._validatePosition(pos); }, - getWordRangeAtPosition(pos, regexp?) { return data._getWordRangeAtPosition(pos, regexp); } + get uri() { return that._uri; }, + get fileName() { return that._uri.fsPath; }, + get isUntitled() { return that._uri.scheme === Schemas.untitled; }, + get languageId() { return that._languageId; }, + get version() { return that._versionId; }, + get isClosed() { return that._isDisposed; }, + get isDirty() { return that._isDirty; }, + get notebook() { return that._notebook; }, + save() { return that._save(); }, + getText(range?) { return range ? that._getTextInRange(range) : that.getText(); }, + get eol() { return that._eol === '\n' ? EndOfLine.LF : EndOfLine.CRLF; }, + get lineCount() { return that._lines.length; }, + lineAt(lineOrPos: number | vscode.Position) { return that._lineAt(lineOrPos); }, + offsetAt(pos) { return that._offsetAt(pos); }, + positionAt(offset) { return that._positionAt(offset); }, + validateRange(ran) { return that._validateRange(ran); }, + validatePosition(pos) { return that._validatePosition(pos); }, + getWordRangeAtPosition(pos, regexp?) { return that._getWordRangeAtPosition(pos, regexp); }, }; } return Object.freeze(this._document); diff --git a/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts b/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts index 1518dc43d00..e378b49a198 100644 --- a/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts +++ b/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'vs/base/common/assert'; +import * as vscode from 'vscode'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta, IModelAddedData, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor'; @@ -29,6 +30,14 @@ class Reference { } } +export interface IExtHostModelAddedData extends IModelAddedData { + notebook?: vscode.NotebookDocument; +} + +export interface IExtHostDocumentsAndEditorsDelta extends IDocumentsAndEditorsDelta { + addedDocuments?: IExtHostModelAddedData[]; +} + export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsShape { readonly _serviceBrand: undefined; @@ -54,6 +63,10 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha ) { } $acceptDocumentsAndEditorsDelta(delta: IDocumentsAndEditorsDelta): void { + this.acceptDocumentsAndEditorsDelta(delta); + } + + acceptDocumentsAndEditorsDelta(delta: IExtHostDocumentsAndEditorsDelta): void { const removedDocuments: ExtHostDocumentData[] = []; const addedDocuments: ExtHostDocumentData[] = []; @@ -88,9 +101,10 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha resource, data.lines, data.EOL, - data.modeId, data.versionId, - data.isDirty + data.modeId, + data.isDirty, + data.notebook )); this._documents.set(resource, ref); addedDocuments.push(ref.value); diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 8c5266dc2d8..037b0c71e63 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -17,7 +17,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { CellKind, ExtHostNotebookShape, IMainContext, IModelAddedData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; @@ -61,14 +61,15 @@ const addIdToOutput = (output: IRawOutput, id = UUID.generateUuid()): IProcessed export class ExtHostCell extends Disposable implements vscode.NotebookCell { - public static asModelAddData(cell: IMainCellDto): IModelAddedData { + public static asModelAddData(notebook: ExtHostNotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { return { EOL: cell.eol, lines: cell.source, modeId: cell.language, uri: cell.uri, isDirty: false, - versionId: 1 + versionId: 1, + notebook }; } @@ -363,7 +364,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo } const contentChangeEvents: vscode.NotebookCellsChangeData[] = []; - const addedCellDocuments: IModelAddedData[] = []; + const addedCellDocuments: IExtHostModelAddedData[] = []; splices.reverse().forEach(splice => { const cellDtos = splice[2]; @@ -372,7 +373,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell); if (!initialization) { - addedCellDocuments.push(ExtHostCell.asModelAddData(cell)); + addedCellDocuments.push(ExtHostCell.asModelAddData(this, cell)); } if (!this._cellDisposableMapping.has(extCell.handle)) { @@ -404,7 +405,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo }); if (addedCellDocuments) { - this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ addedDocuments: addedCellDocuments }); + this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ addedDocuments: addedCellDocuments }); } if (!initialization) { @@ -963,7 +964,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN filter: vscode.NotebookOutputSelector, renderer: vscode.NotebookOutputRenderer ): vscode.Disposable { - if (this._notebookKernels.has(type)) { + if (this._notebookOutputRenderers.has(type)) { throw new Error(`Notebook renderer for '${type}' already registered`); } @@ -1588,7 +1589,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); // add cell document as vscode.TextDocument - addedCellDocuments.push(...modelData.cells.map(ExtHostCell.asModelAddData)); + addedCellDocuments.push(...modelData.cells.map(cell => ExtHostCell.asModelAddData(document, cell))); this._documents.get(revivedUri)?.dispose(); this._documents.set(revivedUri, document); @@ -1600,9 +1601,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } - this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ - addedDocuments: addedCellDocuments - }); + this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ addedDocuments: addedCellDocuments }); const document = this._documents.get(revivedUri)!; this._onDidOpenNotebookDocument.fire(document); diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index d49cb264c21..ad449efd0da 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -175,6 +175,9 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi // Nothing changed return false; } + if (cols === 0 || rows === 0) { + return false; + } this._cols = cols; this._rows = rows; return true; diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index e562c83198d..848df29f9ca 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -12,7 +12,7 @@ import { Event } from 'vs/base/common/event'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; -import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { getIEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; @@ -132,7 +132,7 @@ export interface IEditorGroupView extends IDisposable, ISerializableView, IEdito } export function getActiveTextEditorOptions(group: IEditorGroup, expectedActiveEditor?: IEditorInput, presetOptions?: EditorOptions): EditorOptions { - const activeGroupCodeEditor = group.activeEditorPane ? getCodeEditor(group.activeEditorPane.getControl()) : undefined; + const activeGroupCodeEditor = group.activeEditorPane ? getIEditor(group.activeEditorPane.getControl()) : undefined; if (activeGroupCodeEditor) { if (!expectedActiveEditor || expectedActiveEditor.matches(group.activeEditor)) { return TextEditorOptions.fromEditor(activeGroupCodeEditor, presetOptions); diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index bc9f5db8216..d7ae2ba76a1 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -12,7 +12,7 @@ import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IEditorIdentifier, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; -import { GroupDirection, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { GroupDirection, MergeGroupMode, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; import { toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -277,13 +277,13 @@ class DropOverlay extends Themable { pinned: true, // always pin dropped editor sticky: sourceGroup.isSticky(draggedEditor.editor) // preserve sticky state })); - targetGroup.openEditor(draggedEditor.editor, options); + const copyEditor = this.isCopyOperation(event, draggedEditor); + targetGroup.openEditor(draggedEditor.editor, options, copyEditor ? OpenEditorContext.COPY_EDITOR : OpenEditorContext.MOVE_EDITOR); // Ensure target has focus targetGroup.focus(); // Close in source group unless we copy - const copyEditor = this.isCopyOperation(event, draggedEditor); if (!copyEditor) { sourceGroup.closeEditor(draggedEditor.editor); } diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css index 9d01ee4526b..c6be6928d49 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css @@ -115,6 +115,10 @@ text-overflow: ellipsis; } +.monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-text-button { + display: inline-block; /* to enable ellipsis in text overflow */ +} + /** Notification: Progress */ .monaco-workbench .notifications-list-container .progress-bit { diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index b0712b4e90b..3d327ef6f64 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -244,6 +244,7 @@ export interface IAddedViewDescriptorRef extends IViewDescriptorRef { export interface IAddedViewDescriptorState { viewDescriptor: IViewDescriptor, collapsed?: boolean; + visible?: boolean; } export interface IViewContainerModel { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index c2bbe5e9a35..41b877f6723 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -35,6 +35,7 @@ import { isSafari } from 'vs/base/browser/browser'; import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/common/themeService'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { ILabelService } from 'vs/platform/label/common/label'; +import { debugAdapterRegisteredEmitter } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; const $ = dom.$; @@ -167,8 +168,11 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi @ILabelService private readonly labelService: ILabelService ) { this.breakpointWidgetVisible = CONTEXT_BREAKPOINT_WIDGET_VISIBLE.bindTo(contextKeyService); - this.registerListeners(); this.setDecorationsScheduler = new RunOnceScheduler(() => this.setDecorations(), 30); + debugAdapterRegisteredEmitter.event(() => { + this.registerListeners(); + this.setDecorationsScheduler.schedule(); + }); } private registerListeners(): void { diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 1d4f3bf717d..23009759915 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -488,8 +488,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer setActionBar())); - data.stateLabel.hidden = false; + data.stateLabel.style.display = ''; if (thread && thread.stoppedDetails) { data.stateLabel.textContent = thread.stoppedDetails.description || nls.localize('debugStopped', "Paused on {0}", thread.stoppedDetails.reason || ''); @@ -553,12 +552,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer s.parentSession === session); - if (!hasChildSessions) { - data.stateLabel.textContent = nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); - } else { - data.stateLabel.hidden = true; - } + data.stateLabel.textContent = nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); } } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 4db8bcb67a3..7f0411f6483 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -60,12 +60,17 @@ debugAdapterRegisteredEmitter.event(() => { // Register these contributions lazily only once a debug adapter extension has been registered registerWorkbenchContributions(); registerColors(); - registerCommands(); registerCommandsAndActions(); registerDebugMenu(); - registerDebugPanel(); - registerEditorActions(); }); +registerEditorActions(); +registerCommands(); +registerDebugPanel(); +const debugCategory = nls.localize('debugCategory', "Debug"); +const runCategroy = nls.localize('runCategory', "Run"); +registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory); +registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy); + registerSingleton(IDebugService, DebugService, true); registerDebugView(); registerConfiguration(); @@ -101,14 +106,9 @@ function regsiterEditorContributions(): void { function registerCommandsAndActions(): void { - const debugCategory = nls.localize('debugCategory', "Debug"); - const runCategroy = nls.localize('runCategory', "Run"); - - registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory); registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory); registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory); registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy); registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory); registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory); registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index 184cf2c5912..3d699d77e69 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -9,8 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpointsView'; @@ -210,13 +209,13 @@ class SelectionToWatchExpressionsAction extends EditorAction { async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { const debugService = accessor.get(IDebugService); - const viewletService = accessor.get(IViewletService); + const viewsService = accessor.get(IViewsService); if (!editor.hasModel()) { return; } const text = editor.getModel().getValueInRange(editor.getSelection()); - await viewletService.openViewlet(VIEWLET_ID); + await viewsService.openView(WATCH_VIEW_ID); debugService.addWatchExpression(text); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 71cf0e20cc7..fa3df30dc4d 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -28,11 +28,11 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { parse, getFirstFrame } from 'vs/base/common/console'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, Action } from 'vs/base/common/actions'; import { deepClone, equals } from 'vs/base/common/objects'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IDebugModel, IEnablement, IBreakpoint, IBreakpointData, ICompound, IStackFrame, getStateLabel, IDebugSessionOptions, CONTEXT_DEBUG_UX, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, IGlobalConfig } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IDebugModel, IEnablement, IBreakpoint, IBreakpointData, ICompound, IStackFrame, getStateLabel, IDebugSessionOptions, CONTEXT_DEBUG_UX, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, IGlobalConfig, CALLSTACK_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { getExtensionHostDebugSession } from 'vs/workbench/contrib/debug/common/debugUtils'; import { isErrorWithActions } from 'vs/base/common/errorsWithActions'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -41,11 +41,12 @@ import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { TaskRunResult, DebugTaskRunner } from 'vs/workbench/contrib/debug/browser/debugTaskRunner'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; -import { IViewsService } from 'vs/workbench/common/views'; +import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; import { generateUuid } from 'vs/base/common/uuid'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { DebugTelemetry } from 'vs/workbench/contrib/debug/common/debugTelemetry'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export class DebugService implements IDebugService { declare readonly _serviceBrand: undefined; @@ -76,6 +77,7 @@ export class DebugService implements IDebugService { @IEditorService private readonly editorService: IEditorService, @IViewletService private readonly viewletService: IViewletService, @IViewsService private readonly viewsService: IViewsService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @@ -87,7 +89,8 @@ export class DebugService implements IDebugService { @IFileService private readonly fileService: IFileService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService, - @IActivityService private readonly activityService: IActivityService + @IActivityService private readonly activityService: IActivityService, + @ICommandService private readonly commandService: ICommandService ) { this.toDispose = []; @@ -164,7 +167,10 @@ export class DebugService implements IDebugService { this.activity.dispose(); } if (numberOfSessions > 0) { - this.activity = this.activityService.showViewContainerActivity(VIEWLET_ID, { badge: new NumberBadge(numberOfSessions, n => n === 1 ? nls.localize('1activeSession', "1 active session") : nls.localize('nActiveSessions', "{0} active sessions", n)) }); + const viewContainer = this.viewDescriptorService.getViewContainerByViewId(CALLSTACK_VIEW_ID); + if (viewContainer) { + this.activity = this.activityService.showViewContainerActivity(viewContainer.id, { badge: new NumberBadge(numberOfSessions, n => n === 1 ? nls.localize('1activeSession', "1 active session") : nls.localize('nActiveSessions', "{0} active sessions", n)) }); + } } })); this.toDispose.push(this.model.onDidChangeBreakpoints(() => setBreakpointsExistContext())); @@ -438,7 +444,18 @@ export class DebugService implements IDebugService { nls.localize('debugTypeMissing', "Missing property 'type' for the chosen launch configuration."); } - await this.showError(message); + const actionList: IAction[] = []; + + actionList.push(new Action( + 'installAdditionalDebuggers', + nls.localize('installAdditionalDebuggers', "Install {0} Extension", resolvedConfig.type), + undefined, + true, + async () => this.commandService.executeCommand('debug.installAdditionalDebuggers') + )); + + await this.showError(message, actionList); + return false; } diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index 1daf1517449..7b93fdb20c7 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -49,13 +49,13 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } private async openInternal(input: EditorInput, options: EditorOptions | undefined): Promise { - if (input instanceof FileEditorInput) { + if (input instanceof FileEditorInput && this.group) { + + // Enforce to open the input as text to enable our text based viewer input.setForceOpenAsText(); - if (this.group !== undefined) { - await openEditorWith(input, undefined, options, this.group, this.editorService, this.configurationService, this.quickInputService); - } else { - await this.editorService.openEditor(input, options, this.group); - } + + // If more editors are installed that can handle this input, show a picker + await openEditorWith(input, undefined, options, this.group, this.editorService, this.configurationService, this.quickInputService); } } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 08a100254f8..2d940ea198a 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 { compareFileExtensionsNumeric, compareFileNamesNumeric } from 'vs/base/common/comparers'; +import { compareFileNamesDefault, compareFileExtensionsDefault } 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'; @@ -665,7 +665,7 @@ export class FileSorter implements ITreeSorter { } if (statA.isDirectory && statB.isDirectory) { - return compareFileNamesNumeric(statA.name, statB.name); + return compareFileNamesDefault(statA.name, statB.name); } break; @@ -699,17 +699,17 @@ export class FileSorter implements ITreeSorter { // Sort Files switch (sortOrder) { case 'type': - return compareFileExtensionsNumeric(statA.name, statB.name); + return compareFileExtensionsDefault(statA.name, statB.name); case 'modified': if (statA.mtime !== statB.mtime) { return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1; } - return compareFileNamesNumeric(statA.name, statB.name); + return compareFileNamesDefault(statA.name, statB.name); default: /* 'default', 'mixed', 'filesFirst' */ - return compareFileNamesNumeric(statA.name, statB.name); + return compareFileNamesDefault(statA.name, statB.name); } } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 1f030d65559..63f2adeb309 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -38,8 +38,10 @@ const RENDER_ALL_MARKDOWN_CELLS = 'notebook.renderAllMarkdownCells'; // Cell Commands const INSERT_CODE_CELL_ABOVE_COMMAND_ID = 'notebook.cell.insertCodeCellAbove'; const INSERT_CODE_CELL_BELOW_COMMAND_ID = 'notebook.cell.insertCodeCellBelow'; +const INSERT_CODE_CELL_AT_TOP_COMMAND_ID = 'notebook.cell.insertCodeCellAtTop'; const INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID = 'notebook.cell.insertMarkdownCellAbove'; const INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID = 'notebook.cell.insertMarkdownCellBelow'; +const INSERT_MARKDOWN_CELL_AT_TOP_COMMAND_ID = 'notebook.cell.insertMarkdownCellAtTop'; const CHANGE_CELL_TO_CODE_COMMAND_ID = 'notebook.cell.changeToCode'; const CHANGE_CELL_TO_MARKDOWN_COMMAND_ID = 'notebook.cell.changeToMarkdown'; @@ -386,6 +388,13 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellTitle, { group: CellOverflowToolbarGroups.Insert }); +MenuRegistry.appendMenuItem(MenuId.EditorContext, { + submenu: MenuId.NotebookCellTitle, + title: localize('notebookMenu.cellTitle', "Notebook Cell"), + group: CellOverflowToolbarGroups.Insert, + when: NOTEBOOK_EDITOR_FOCUSED +}); + MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: EXECUTE_NOTEBOOK_COMMAND_ID, @@ -570,6 +579,56 @@ registerAction2(class extends InsertCellCommand { } }); +registerAction2(class extends NotebookAction { + constructor() { + super( + { + id: INSERT_CODE_CELL_AT_TOP_COMMAND_ID, + title: localize('notebookActions.insertCodeCellAtTop', "Add Code Cell At Top"), + f1: false + }); + } + + async run(accessor: ServicesAccessor): Promise { + const context = this.getActiveEditorContext(accessor); + if (context) { + this.runWithContext(accessor, context); + } + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + const newCell = context.notebookEditor.insertNotebookCell(undefined, CellKind.Code, 'above', undefined, true); + if (newCell) { + context.notebookEditor.focusNotebookCell(newCell, 'editor'); + } + } +}); + +registerAction2(class extends NotebookAction { + constructor() { + super( + { + id: INSERT_MARKDOWN_CELL_AT_TOP_COMMAND_ID, + title: localize('notebookActions.insertMarkdownCellAtTop', "Add Markdown Cell At Top"), + f1: false + }); + } + + async run(accessor: ServicesAccessor): Promise { + const context = this.getActiveEditorContext(accessor); + if (context) { + this.runWithContext(accessor, context); + } + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + const newCell = context.notebookEditor.insertNotebookCell(undefined, CellKind.Markdown, 'above', undefined, true); + if (newCell) { + context.notebookEditor.focusNotebookCell(newCell, 'editor'); + } + } +}); + MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { command: { id: INSERT_CODE_CELL_BELOW_COMMAND_ID, @@ -580,6 +639,16 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { group: 'inline' }); +MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { + command: { + id: INSERT_CODE_CELL_AT_TOP_COMMAND_ID, + title: localize('notebookActions.menu.insertCode', "$(add) Code"), + tooltip: localize('notebookActions.menu.insertCode.tooltip', "Add Code Cell") + }, + order: 0, + group: 'inline' +}); + registerAction2(class extends InsertCellCommand { constructor() { super( @@ -622,6 +691,16 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { group: 'inline' }); +MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { + command: { + id: INSERT_MARKDOWN_CELL_AT_TOP_COMMAND_ID, + title: localize('notebookActions.menu.insertMarkdown', "$(add) Markdown"), + tooltip: localize('notebookActions.menu.insertMarkdown.tooltip', "Add Markdown Cell") + }, + order: 1, + group: 'inline' +}); + registerAction2(class extends NotebookCellAction { constructor() { super( @@ -1229,7 +1308,7 @@ export class ChangeCellLanguageAction extends NotebookCellAction { const providerLanguages = [...context.notebookEditor.viewModel!.notebookDocument.languages, 'markdown']; providerLanguages.forEach(languageId => { let description: string; - if (languageId === context.cell.language) { + if (context.cell.cellKind === CellKind.Markdown ? (languageId === 'markdown') : (languageId === context.cell.language)) { description = localize('languageDescription', "({0}) - Current Language", languageId); } else { description = localize('languageDescriptionConfigured', "({0})", languageId); @@ -1452,7 +1531,7 @@ registerAction2(class extends NotebookCellAction { }, menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED.toNegated()), + when: ContextKeyExpr.and(NOTEBOOK_CELL_INPUT_COLLAPSED.toNegated()), group: CellOverflowToolbarGroups.Collapse, } }); @@ -1475,7 +1554,7 @@ registerAction2(class extends NotebookCellAction { }, menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED), + when: ContextKeyExpr.and(NOTEBOOK_CELL_INPUT_COLLAPSED), group: CellOverflowToolbarGroups.Collapse, } }); @@ -1498,7 +1577,7 @@ registerAction2(class extends NotebookCellAction { }, menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED.toNegated(), NOTEBOOK_CELL_HAS_OUTPUTS), + when: ContextKeyExpr.and(NOTEBOOK_CELL_OUTPUT_COLLAPSED.toNegated(), NOTEBOOK_CELL_HAS_OUTPUTS), group: CellOverflowToolbarGroups.Collapse, } }); @@ -1521,7 +1600,7 @@ registerAction2(class extends NotebookCellAction { }, menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED), + when: ContextKeyExpr.and(NOTEBOOK_CELL_OUTPUT_COLLAPSED), group: CellOverflowToolbarGroups.Collapse, } }); diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 80e04e4ae67..6d3c9fbc887 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -136,6 +136,7 @@ transform: translate3d(0px, 0px, 0px); cursor: auto; box-sizing: border-box; + z-index: 27; /* Over drag handle */ } .monaco-workbench .notebookOverlay .output p { @@ -152,7 +153,7 @@ .monaco-workbench .notebookOverlay .output > div.foreground > .output-inner-container { width: 100%; - padding: 8px; + padding: 4px 8px; box-sizing: border-box; } @@ -175,7 +176,6 @@ height: 16px; cursor: pointer; padding: 4px; - z-index: 27; } .monaco-workbench .notebookOverlay .output .error_message { @@ -234,6 +234,7 @@ position: relative; left: -23px; cursor: pointer; + z-index: 27; /* Over drag handle */ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.collapsed .notebook-folding-indicator, @@ -513,6 +514,12 @@ opacity: 0.5 !important; } + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container.emptyNotebook { + opacity: 1 !important; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { position: absolute; display: flex; @@ -528,23 +535,29 @@ display: none; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:focus-within, +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:hover, .monaco-workbench .notebookOverlay.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 .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 .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar { margin: 0px 8px; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-item, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-item { display: flex; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-item.active, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-item.active { transform: none; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-label { font-size: 12px; margin: 0px; @@ -552,19 +565,23 @@ padding: 0px 4px; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label .codicon, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-label .codicon { margin-right: 3px; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-action-bar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-action-bar { display: flex; align-items: center; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item:first-child, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .action-item:first-child { margin-right: 16px; } +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container span.codicon, .monaco-workbench .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; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 878152d3ffe..c51b949525b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -import { IListEvent, IListMouseEvent } from 'vs/base/browser/ui/list/list'; +import { IListContextMenuEvent, IListEvent, IListMouseEvent } from 'vs/base/browser/ui/list/list'; import { IListOptions, IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; @@ -190,6 +190,7 @@ export interface INotebookEditor extends IEditor { readonly onDidChangeAvailableKernels: Event; readonly onDidChangeKernel: Event; readonly onDidChangeActiveCell: Event; + readonly onDidScroll: Event; isDisposed: boolean; @@ -424,6 +425,7 @@ export interface INotebookCellList { elementAt(position: number): ICellViewModel | undefined; elementHeight(element: ICellViewModel): number; onWillScroll: Event; + onDidScroll: Event; onDidChangeFocus: Event>; onDidChangeContentHeight: Event; scrollTop: number; @@ -435,6 +437,7 @@ export interface INotebookCellList { readonly onDidHideOutput: Event; readonly onMouseUp: Event>; readonly onMouseDown: Event>; + readonly onContextMenu: Event>; detachViewModel(): void; attachViewModel(viewModel: NotebookViewModel): void; clear(): void; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 6ae7a4abea0..669fc06564a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -33,7 +33,7 @@ import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/ 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 { CellDragAndDropController, CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate, ListTopCellToolbar } 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'; @@ -51,6 +51,14 @@ import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/deb import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { notebookKernelProviderAssociationsSettingId, NotebookKernelProviderAssociations } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; +import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IAction, Separator } from 'vs/base/common/actions'; +import { isMacintosh, isNative } from 'vs/base/common/platform'; +import { getTitleBarStyle } from 'vs/platform/windows/common/windows'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ScrollEvent } from 'vs/base/common/scrollable'; const $ = DOM.$; @@ -83,6 +91,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _webviewTransparentCover: HTMLElement | null = null; private _list: INotebookCellList | undefined; private _dndController: CellDragAndDropController | null = null; + private _listTopCellToolbar: ListTopCellToolbar | null = null; private _renderedEditors: Map = new Map(); private _eventDispatcher: NotebookEventDispatcher | undefined; private _notebookViewModel: NotebookViewModel | undefined; @@ -195,6 +204,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private readonly _onDidChangeActiveCell = this._register(new Emitter()); readonly onDidChangeActiveCell: Event = this._onDidChangeActiveCell.event; + private readonly _onDidScroll = this._register(new Emitter()); + + readonly onDidScroll: Event = this._onDidScroll.event; private _cursorNavigationMode: boolean = false; get cursorNavigationMode(): boolean { @@ -210,8 +222,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor @IStorageService storageService: IStorageService, @INotebookService private notebookService: INotebookService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, @IContextKeyService readonly contextKeyService: IContextKeyService, - @ILayoutService private readonly layoutService: ILayoutService + @ILayoutService private readonly layoutService: ILayoutService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IMenuService private readonly menuService: IMenuService, ) { super(); this._memento = new Memento(NotebookEditorWidget.ID, storageService); @@ -395,7 +410,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor multipleSelectionSupport: false, enableKeyboardNavigation: true, additionalScrollHeight: 0, - transformOptimization: true, + transformOptimization: (isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', styleController: (_suffix: string) => { return this._list!; }, overrideStyles: { listBackground: editorBackground, @@ -433,6 +448,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._register(this._list); this._register(combinedDisposable(...renderers)); + // top cell toolbar + this._listTopCellToolbar = this._register(this.instantiationService.createInstance(ListTopCellToolbar, this, this._list.rowsContainer)); + // transparent cover this._webviewTransparentCover = DOM.append(this._list.rowsContainer, $('.webview-cover')); this._webviewTransparentCover.style.display = 'none'; @@ -468,10 +486,38 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._cursorNavigationMode = false; })); + this._register(this._list.onContextMenu(e => { + this.showListContextMenu(e); + })); + + this._register(this._list.onDidScroll((e) => { + this._onDidScroll.fire(e); + })); + const widgetFocusTracker = DOM.trackFocus(this.getDomNode()); this._register(widgetFocusTracker); this._register(widgetFocusTracker.onDidFocus(() => this._onDidFocusEmitter.fire())); + } + private showListContextMenu(e: IListContextMenuEvent) { + this.contextMenuService.showContextMenu({ + getActions: () => { + const result: IAction[] = []; + const menu = this.menuService.createMenu(MenuId.NotebookCellTitle, this.contextKeyService); + const groups = menu.getActions(); + menu.dispose(); + + for (let group of groups) { + const [, actions] = group; + result.push(...actions); + result.push(new Separator()); + } + + result.pop(); // remove last separator + return result; + }, + getAnchor: () => e.anchor + }); } private _updateForCursorNavigationMode(applyFocusChange: () => void): void { @@ -596,18 +642,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } } - } else if (this._notebookViewModel && this._notebookViewModel.viewCells.length === 1 && this._notebookViewModel.viewCells[0].cellKind === CellKind.Code) { - // there is only one code cell in the document - const cell = this._notebookViewModel!.viewCells[0]; - if (cell.getTextLength() === 0) { - // the cell is empty, very likely a template cell, focus it - this.selectElement(cell); - await this.revealLineInCenterAsync(cell, 1); - const editor = this._renderedEditors.get(cell)!; - if (editor) { - editor.focus(); - } - } } } @@ -1634,6 +1668,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._localStore.clear(); this._list?.dispose(); + this._listTopCellToolbar?.dispose(); this._overlayContainer.remove(); this.viewModel?.dispose(); @@ -1802,12 +1837,14 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .monaco-list-row .cell-title-toolbar { background-color: ${editorBackgroundColor}; }`); collector.addRule(`.notebookOverlay .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item { background-color: ${editorBackgroundColor} }`); + collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container .action-item { background-color: ${editorBackgroundColor} }`); } const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); if (cellToolbarSeperator) { collector.addRule(`.notebookOverlay .monaco-list-row .cell-title-toolbar { border: solid 1px ${cellToolbarSeperator}; }`); collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item { border: solid 1px ${cellToolbarSeperator} }`); + collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container .action-item { border: solid 1px ${cellToolbarSeperator} }`); collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { border-bottom: solid 1px ${cellToolbarSeperator} }`); collector.addRule(`.notebookOverlay .monaco-action-bar .action-item.verticalSeparator { background-color: ${cellToolbarSeperator} }`); } @@ -1918,4 +1955,5 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${CELL_BOTTOM_MARGIN}px; }`); collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { margin-left: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; height: ${COLLAPSED_INDICATOR_HEIGHT}px; }`); + collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${SCROLLABLE_ELEMENT_PADDING_TOP}px }`); }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts index c1f42f14d6c..af6c59fc4ed 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts @@ -19,7 +19,7 @@ export class PureNotebookOutputRenderer implements INotebookRendererInfo { public readonly preloads: URI[]; - constructor(public readonly id: string, extension: IExtensionDescription, entrypoint: string) { + constructor(public readonly id: string, public readonly displayName: string, extension: IExtensionDescription, entrypoint: string) { this.extensionId = extension.identifier; this.extensionLocation = extension.extensionLocation; this.preloads = [joinPath(extension.extensionLocation, entrypoint)]; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 99c441658f7..a45e3a397c0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -298,7 +298,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu })); if (notebookContribution.entrypoint) { - this._notebookRenderers.set(notebookContribution.viewType, new PureNotebookOutputRenderer(notebookContribution.viewType, extension.description, notebookContribution.entrypoint)); + this._notebookRenderers.set(notebookContribution.viewType, new PureNotebookOutputRenderer(notebookContribution.viewType, notebookContribution.displayName, extension.description, notebookContribution.entrypoint)); } } } @@ -361,6 +361,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu if (CopyAction) { this._register(CopyAction.addImplementation(PRIORITY, accessor => { + const activeElement = document.activeElement; + if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { + return false; + } + const { editor, activeCell } = getContext(); if (!editor || !activeCell) { return false; @@ -382,6 +387,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu if (PasteAction) { PasteAction.addImplementation(PRIORITY, () => { + const activeElement = document.activeElement; + if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { + return false; + } + const pasteCells = this.getToCopy(); if (!pasteCells) { @@ -416,7 +426,15 @@ export class NotebookService extends Disposable implements INotebookService, ICu cell.language, cell.cellKind, [], - cell.metadata + { + editable: cell.metadata?.editable, + runnable: cell.metadata?.runnable, + breakpointMargin: cell.metadata?.breakpointMargin, + hasExecutionOrder: cell.metadata?.hasExecutionOrder, + inputCollapsed: cell.metadata?.inputCollapsed, + outputCollapsed: cell.metadata?.outputCollapsed, + custom: cell.metadata?.custom + } ); } else { return cell; @@ -465,6 +483,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu if (CutAction) { CutAction.addImplementation(PRIORITY, accessor => { + const activeElement = document.activeElement; + if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { + return false; + } + const { editor, activeCell } = getContext(); if (!editor || !activeCell) { return false; @@ -524,6 +547,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu registerNotebookRenderer(id: string, renderer: INotebookRendererInfo) { this._notebookRenderers.set(id, renderer); + const staticInfo = this.notebookRenderersInfoStore.get(id); + + if (staticInfo) { + + } } unregisterNotebookRenderer(id: string) { @@ -972,8 +1000,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu return this.notebookProviderInfoStore.get(viewType); } - getContributedNotebookOutputRenderers(mimeType: string): readonly NotebookOutputRendererInfo[] { - return this.notebookRenderersInfoStore.getContributedRenderer(mimeType); + getContributedNotebookOutputRenderers(viewType: string): NotebookOutputRendererInfo | undefined { + return this.notebookRenderersInfoStore.get(viewType); } getNotebookProviderResourceRoots(): URI[] { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts index c5ccf32bc91..69e4ddad204 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts @@ -29,21 +29,22 @@ export class VerticalSeparatorViewItem extends BaseActionViewItem { } } -export function createAndFillInActionBarActionsWithVerticalSeparators(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable { +export function createAndFillInActionBarActionsWithVerticalSeparators(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, alwaysFillSecondary?: boolean, isPrimaryGroup?: (group: string) => boolean): IDisposable { const groups = menu.getActions(options); // Action bars handle alternative actions on their own so the alternative actions should be ignored - fillInActions(groups, target, false, isPrimaryGroup); + fillInActions(groups, target, false, alwaysFillSecondary, isPrimaryGroup); return asDisposable(groups); } -function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { +function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, alwaysFillSecondary = false, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { for (const tuple of groups) { let [group, actions] = tuple; if (useAlternativeActions) { actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a); } - if (isPrimaryGroup(group)) { + const isPrimary = isPrimaryGroup(group); + if (isPrimary) { const to = Array.isArray(target) ? target : target.primary; if (to.length > 0) { @@ -51,7 +52,9 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray(target) ? target : target.secondary; if (to.length > 0) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts index 1060d348525..945ba2e54c8 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts @@ -124,10 +124,8 @@ export class CellContextKeyManager extends Disposable { private updateForOutputs() { if (this.element instanceof CodeCellViewModel) { - console.log(this.element, this.element.outputs.length > 0); this.cellHasOutputs.set(this.element.outputs.length > 0); } else { - console.log(this.element, false); this.cellHasOutputs.set(false); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts index 8c3f6e513b5..acd53605379 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts @@ -19,6 +19,10 @@ export class CellMenus { return this.getMenu(MenuId.NotebookCellBetween, contextKeyService); } + getCellTopInsertionMenu(contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.NotebookCellListTop, contextKeyService); + } + private getMenu(menuId: MenuId, contextKeyService: IContextKeyService): IMenu { const menu = this.menuService.createMenu(menuId, contextKeyService); 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 c36c13e1ad6..f75607d8563 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -209,7 +209,7 @@ abstract class AbstractCellRenderer { const cellMenu = this.instantiationService.createInstance(CellMenus); const menu = disposables.add(cellMenu.getCellInsertionMenu(contextKeyService)); - const actions = this.getCellToolbarActions(menu); + const actions = this.getCellToolbarActions(menu, false); toolbar.setActions(actions.primary, actions.secondary); return toolbar; @@ -254,19 +254,19 @@ abstract class AbstractCellRenderer { return toolbar; } - private getCellToolbarActions(menu: IMenu): { primary: IAction[], secondary: IAction[] } { + private getCellToolbarActions(menu: IMenu, alwaysFillSecondaryActions: boolean): { primary: IAction[], secondary: IAction[] } { const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary }; - createAndFillInActionBarActionsWithVerticalSeparators(menu, { shouldForwardArgs: true }, result, g => /^inline/.test(g)); + createAndFillInActionBarActionsWithVerticalSeparators(menu, { shouldForwardArgs: true }, result, alwaysFillSecondaryActions, g => /^inline/.test(g)); return result; } protected setupCellToolbarActions(templateData: BaseCellRenderTemplate, disposables: DisposableStore): void { const updateActions = () => { - const actions = this.getCellToolbarActions(templateData.titleMenu); + const actions = this.getCellToolbarActions(templateData.titleMenu, true); const hadFocus = DOM.isAncestor(document.activeElement, templateData.toolbar.getElement()); templateData.toolbar.setActions(actions.primary, actions.secondary); @@ -813,7 +813,7 @@ export class CellLanguageStatusBarItem extends Disposable { } private render(): void { - const modeId = this.modeService.getModeIdForLanguageName(this.cell!.language) || this.cell!.language; + const modeId = this.cell?.cellKind === CellKind.Markdown ? 'markdown' : this.modeService.getModeIdForLanguageName(this.cell!.language) || this.cell!.language; this.labelElement.textContent = this.modeService.getLanguageName(modeId) || this.modeService.getLanguageName('plaintext'); } } @@ -1291,3 +1291,71 @@ export class RunStateRenderer { } } } + +export class ListTopCellToolbar extends Disposable { + private topCellToolbar: HTMLElement; + private _modelDisposables = new DisposableStore(); + constructor( + protected readonly notebookEditor: INotebookEditor, + + insertionIndicatorContainer: HTMLElement, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @INotificationService private readonly notificationService: INotificationService, + @IContextKeyService readonly contextKeyService: IContextKeyService, + ) { + super(); + + this.topCellToolbar = DOM.append(insertionIndicatorContainer, $('.cell-list-top-cell-toolbar-container')); + + const toolbar = new ToolBar(this.topCellToolbar, this.contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return item; + } + + return undefined; + } + }); + + const cellMenu = this.instantiationService.createInstance(CellMenus); + const menu = this._register(cellMenu.getCellTopInsertionMenu(contextKeyService)); + + const actions = this.getCellToolbarActions(menu, false); + toolbar.setActions(actions.primary, actions.secondary); + + this._register(toolbar); + + this._register(this.notebookEditor.onDidChangeModel(() => { + this._modelDisposables.clear(); + + if (this.notebookEditor.viewModel) { + this._modelDisposables.add(this.notebookEditor.viewModel.onDidChangeViewCells(() => { + this.updateClass(); + })); + } + })); + + this.updateClass(); + } + + private updateClass() { + if (this.notebookEditor.viewModel?.length === 0) { + DOM.addClass(this.topCellToolbar, 'emptyNotebook'); + } else { + DOM.removeClass(this.topCellToolbar, 'emptyNotebook'); + } + } + + private getCellToolbarActions(menu: IMenu, alwaysFillSecondaryActions: boolean): { primary: IAction[], secondary: IAction[] } { + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + createAndFillInActionBarActionsWithVerticalSeparators(menu, { shouldForwardArgs: true }, result, alwaysFillSecondaryActions, g => /^inline/.test(g)); + + return result; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index c541cb9b136..ef7e8c202a1 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -558,7 +558,8 @@ export class CodeCell extends Disposable { const renderInfo = this.notebookService.getRendererInfo(renderId); if (renderInfo) { - return `${renderId} (${renderInfo.extensionId.value})`; + const displayName = renderInfo.displayName !== '' ? renderInfo.displayName : renderInfo.id; + return `${displayName} (${renderInfo.extensionId.value})`; } return nls.localize('builtinRenderInfo', "built-in"); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 46c9f3f0ec5..319c8fd9130 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -113,6 +113,7 @@ export interface INotebookMimeTypeSelector { export interface INotebookRendererInfo { id: string; + displayName: string; extensionId: ExtensionIdentifier; extensionLocation: URI, preloads: URI[], diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 9c6d199d7ed..87fad7fb364 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -16,6 +16,7 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; export const INotebookService = createDecorator('notebookService'); @@ -61,7 +62,9 @@ export interface INotebookService { registerNotebookKernelProvider(provider: INotebookKernelProvider): IDisposable; getContributedNotebookKernels(viewType: string, resource: URI): readonly INotebookKernelInfo[]; getContributedNotebookKernels2(viewType: string, resource: URI, token: CancellationToken): Promise; + getContributedNotebookOutputRenderers(id: string): NotebookOutputRendererInfo | undefined; getRendererInfo(id: string): INotebookRendererInfo | undefined; + resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise; getNotebookTextModel(uri: URI): NotebookTextModel | undefined; executeNotebook(viewType: string, uri: URI): Promise; diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 71fcf82ed16..7f43e90969f 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -33,6 +33,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { ScrollEvent } from 'vs/base/common/scrollable'; export class TestCell extends NotebookCellTextModel { constructor( @@ -69,6 +70,7 @@ export class TestNotebookEditor implements INotebookEditor { multipleKernelsAvailable: boolean = false; onDidChangeAvailableKernels: Event = new Emitter().event; onDidChangeActiveCell: Event = new Emitter().event; + onDidScroll = new Emitter().event; uri?: URI | undefined; textModel?: NotebookTextModel | undefined; diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index f5a22758da5..bf905250784 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -62,6 +62,7 @@ viewsRegistry.registerViews([{ workspace: true, canMoveView: true, weight: 80, + order: -999, containerIcon: Codicon.sourceControl.classNames }], viewContainer); diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index adb459fb155..e20d202956a 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -46,7 +46,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ScrollType, IEditor, ICodeEditorViewState, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; import { once } from 'vs/base/common/functional'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { getIEditor } from 'vs/editor/browser/editorBrowser'; import { withNullAsUndefined } from 'vs/base/common/types'; import { Codicon, stripCodicons } from 'vs/base/common/codicons'; @@ -134,7 +134,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index 82f060e64cc..e39451c4b69 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -35,13 +35,13 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { private _charMeasureElement: HTMLElement | undefined; private _lastFontMeasurement: ITerminalFont | undefined; + private _linuxDistro: LinuxDistro = LinuxDistro.Unknown; public config!: ITerminalConfiguration; private readonly _onWorkspacePermissionsChanged = new Emitter(); public get onWorkspacePermissionsChanged(): Event { return this._onWorkspacePermissionsChanged.event; } public constructor( - private readonly _linuxDistro: LinuxDistro, @IConfigurationService private readonly _configurationService: IConfigurationService, @IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService, @INotificationService private readonly _notificationService: INotificationService, @@ -62,6 +62,10 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { storageKeysSyncRegistryService.registerStorageKey({ key: 'terminalConfigHelper/launchRecommendationsIgnore', version: 1 }); } + public setLinuxDistro(linuxDistro: LinuxDistro) { + this._linuxDistro = linuxDistro; + } + private _updateConfig(): void { this.config = this._configurationService.getValue(TERMINAL_CONFIG_SECTION); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index ee16eb81e97..01fbd5bded8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -98,6 +98,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _titleReadyPromise: Promise; private _titleReadyComplete: ((title: string) => any) | undefined; private _areLinksReady: boolean = false; + private _initialDataEvents: string[] | undefined = []; private _messageTitleDisposable: IDisposable | undefined; @@ -131,6 +132,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // TODO: Should this be an event as it can fire twice? public get processReady(): Promise { return this._processManager.ptyProcessReady; } public get areLinksReady(): boolean { return this._areLinksReady; } + public get initialDataEvents(): string[] | undefined { return this._initialDataEvents; } public get exitCode(): number | undefined { return this._exitCode; } public get title(): string { return this._title; } public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; } @@ -231,6 +233,20 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.updateAccessibilitySupport(); } })); + + // Clear out initial data events after 10 seconds, hopefully extension hosts are up and + // running at that point. + let initialDataEventsTimeout: number | undefined = window.setTimeout(() => { + initialDataEventsTimeout = undefined; + this._initialDataEvents = undefined; + }, 10000); + this._register({ + dispose: () => { + if (initialDataEventsTimeout) { + window.clearTimeout(initialDataEventsTimeout); + } + } + }); } public addDisposable(disposable: IDisposable): void { @@ -862,11 +878,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { protected _createProcessManager(): void { this._processManager = this._instantiationService.createInstance(TerminalProcessManager, this._id, this._configHelper); - this._processManager.onProcessReady(() => { - this._onProcessIdReady.fire(this); - }); + this._processManager.onProcessReady(() => this._onProcessIdReady.fire(this)); this._processManager.onProcessExit(exitCode => this._onProcessExit(exitCode)); - this._processManager.onProcessData(data => this._onData.fire(data)); + this._processManager.onProcessData(data => { + this._initialDataEvents?.push(data); + this._onData.fire(data); + }); this._processManager.onProcessOverrideDimensions(e => this.setDimensions(e)); this._processManager.onProcessResolvedShellLaunchConfig(e => this._setResolvedShellLaunchConfig(e)); this._processManager.onEnvironmentVariableInfoChanged(e => this._onEnvironmentVariableInfoChanged(e)); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 2fcce1787c2..15154160cbd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { TERMINAL_VIEW_ID, IShellLaunchConfig, ITerminalConfigHelper, ITerminalNativeService, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, ITerminalProcessExtHostProxy, IShellDefinition, LinuxDistro, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_VIEW_ID, IShellLaunchConfig, ITerminalConfigHelper, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, ITerminalProcessExtHostProxy, IShellDefinition, LinuxDistro, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, ITerminalLaunchError, ITerminalNativeWindowsDelegate } from 'vs/workbench/contrib/terminal/common/terminal'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; import { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType, ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -59,6 +59,7 @@ export class TerminalService implements ITerminalService { private _configHelper: TerminalConfigHelper; private _terminalContainer: HTMLElement | undefined; + private _nativeWindowsDelegate: ITerminalNativeWindowsDelegate | undefined; public get configHelper(): ITerminalConfigHelper { return this._configHelper; } @@ -91,8 +92,6 @@ export class TerminalService implements ITerminalService { private readonly _onRequestAvailableShells = new Emitter(); public get onRequestAvailableShells(): Event { return this._onRequestAvailableShells.event; } - private readonly _terminalNativeService: ITerminalNativeService | undefined; - constructor( @IContextKeyService private _contextKeyService: IContextKeyService, @IWorkbenchLayoutService private _layoutService: IWorkbenchLayoutService, @@ -104,34 +103,17 @@ export class TerminalService implements ITerminalService { @IQuickInputService private _quickInputService: IQuickInputService, @IConfigurationService private _configurationService: IConfigurationService, @IViewsService private _viewsService: IViewsService, - @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, - // HACK: Ideally TerminalNativeService would depend on TerminalService and inject the - // additional native functionality into it. - @optional(ITerminalNativeService) terminalNativeService: ITerminalNativeService + @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService ) { - // @optional could give undefined and properly typing it breaks service registration - this._terminalNativeService = terminalNativeService as ITerminalNativeService | undefined; - this._activeTabIndex = 0; this._isShuttingDown = false; this._findState = new FindReplaceState(); lifecycleService.onBeforeShutdown(async event => event.veto(this._onBeforeShutdown())); lifecycleService.onShutdown(() => this._onShutdown()); - if (this._terminalNativeService) { - this._terminalNativeService.onRequestFocusActiveInstance(() => { - if (this.terminalInstances.length > 0) { - const terminal = this.getActiveInstance(); - if (terminal) { - terminal.focus(); - } - } - }); - this._terminalNativeService.onOsResume(() => this._onOsResume()); - } this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this._contextKeyService); this._terminalShellTypeContextKey = KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE.bindTo(this._contextKeyService); this._findWidgetVisible = KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE.bindTo(this._contextKeyService); - this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper, this._terminalNativeService?.linuxDistro || LinuxDistro.Unknown); + this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper); this.onTabDisposed(tab => this._removeTab(tab)); this.onActiveTabChanged(() => { const instance = this.getActiveInstance(); @@ -142,6 +124,14 @@ export class TerminalService implements ITerminalService { this._handleContextKeys(); } + public setNativeWindowsDelegate(delegate: ITerminalNativeWindowsDelegate): void { + this._nativeWindowsDelegate = delegate; + } + + public setLinuxDistro(linuxDistro: LinuxDistro): void { + this._configHelper.setLinuxDistro(linuxDistro); + } + private _handleContextKeys(): void { const terminalIsOpenContext = KEYBINDING_CONTEXT_TERMINAL_IS_OPEN.bindTo(this._contextKeyService); @@ -225,14 +215,6 @@ export class TerminalService implements ITerminalService { this.terminalInstances.forEach(instance => instance.dispose(true)); } - private _onOsResume(): void { - const activeTab = this.getActiveTab(); - if (!activeTab) { - return; - } - activeTab.terminalInstances.forEach(instance => instance.forceRedraw()); - } - public getTabLabels(): string[] { return this._terminalTabs.filter(tab => tab.terminalInstances.length > 0).map((tab, index) => `${index + 1}: ${tab.title ? tab.title : ''}`); } @@ -543,8 +525,8 @@ export class TerminalService implements ITerminalService { return; } else if (shellType === WindowsShellType.Wsl) { - if (this._terminalNativeService && this._terminalNativeService.getWindowsBuildNumber() >= 17063) { - c(this._terminalNativeService.getWslPath(originalPath)); + if (this._nativeWindowsDelegate && this._nativeWindowsDelegate.getWindowsBuildNumber() >= 17063) { + c(this._nativeWindowsDelegate.getWslPath(originalPath)); } else { c(originalPath.replace(/\\/g, '/')); } @@ -558,9 +540,9 @@ export class TerminalService implements ITerminalService { } } else { const lowerExecutable = executable.toLowerCase(); - if (this._terminalNativeService && this._terminalNativeService.getWindowsBuildNumber() >= 17063 && + if (this._nativeWindowsDelegate && this._nativeWindowsDelegate.getWindowsBuildNumber() >= 17063 && (lowerExecutable.indexOf('wsl') !== -1 || (lowerExecutable.indexOf('bash.exe') !== -1 && lowerExecutable.toLowerCase().indexOf('git') === -1))) { - c(this._terminalNativeService.getWslPath(originalPath)); + c(this._nativeWindowsDelegate.getWslPath(originalPath)); return; } else if (hasSpace) { c('"' + originalPath + '"'); diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index e0d0fb53029..5edbd197dbd 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -7,7 +7,6 @@ import * as nls from 'vs/nls'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { OperatingSystem } from 'vs/base/common/platform'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -55,8 +54,6 @@ export const NEVER_MEASURE_RENDER_TIME_STORAGE_KEY = 'terminal.integrated.neverM // trying to create the corressponding object on the ext host. export const EXT_HOST_CREATION_DELAY = 100; -export const ITerminalNativeService = createDecorator('terminalNativeService'); - export const TerminalCursorStyle = { BLOCK: 'block', LINE: 'line', @@ -230,18 +227,17 @@ export interface IShellLaunchConfig { } /** - * Provides access to native or electron APIs to other terminal services. + * Provides access to native Windows calls that can be injected into non-native layers. */ -export interface ITerminalNativeService { - readonly _serviceBrand: undefined; - - readonly linuxDistro: LinuxDistro; - - readonly onRequestFocusActiveInstance: Event; - readonly onOsResume: Event; - +export interface ITerminalNativeWindowsDelegate { + /** + * Gets the Windows build number, eg. this would be `19041` for Windows 10 version 2004 + */ getWindowsBuildNumber(): number; - whenFileDeleted(path: URI): Promise; + /** + * Converts a regular Windows path into the WSL path equivalent, eg. `C:\` -> `/mnt/c` + * @param path The Windows path. + */ getWslPath(path: string): Promise; } diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts index 1721bf226a6..263c15cb666 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts @@ -3,22 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalInstanceService } from 'vs/workbench/contrib/terminal/electron-browser/terminalInstanceService'; import { getSystemShell } from 'vs/workbench/contrib/terminal/node/terminal'; -import { TerminalNativeService } from 'vs/workbench/contrib/terminal/electron-browser/terminalNativeService'; -import { ITerminalNativeService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TerminalNativeContribution } from 'vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; import { getTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; // This file contains additional desktop-only contributions on top of those in browser/ // Register services -registerSingleton(ITerminalNativeService, TerminalNativeService, true); registerSingleton(ITerminalInstanceService, TerminalInstanceService, true); +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(TerminalNativeContribution, LifecyclePhase.Ready); + // Register configurations const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration(getTerminalShellConfiguration(getSystemShell)); diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution.ts similarity index 71% rename from src/vs/workbench/contrib/terminal/electron-browser/terminalNativeService.ts rename to src/vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution.ts index 84030ff44a7..52d755f78b2 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution.ts @@ -5,40 +5,40 @@ import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IOpenFileRequest } from 'vs/platform/windows/common/windows'; -import { ITerminalNativeService, LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { getWindowsBuildNumber, linuxDistro } from 'vs/workbench/contrib/terminal/node/terminal'; import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { execFile } from 'child_process'; -import { Emitter, Event } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { registerRemoteContributions } from 'vs/workbench/contrib/terminal/electron-browser/terminalRemote'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { Disposable } from 'vs/base/common/lifecycle'; import { INativeOpenFileRequest } from 'vs/platform/windows/node/window'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -export class TerminalNativeService extends Disposable implements ITerminalNativeService { +export class TerminalNativeContribution extends Disposable implements IWorkbenchContribution { public _serviceBrand: undefined; - public get linuxDistro(): LinuxDistro { return linuxDistro; } - - private readonly _onRequestFocusActiveInstance = this._register(new Emitter()); - public get onRequestFocusActiveInstance(): Event { return this._onRequestFocusActiveInstance.event; } - private readonly _onOsResume = this._register(new Emitter()); - public get onOsResume(): Event { return this._onOsResume.event; } - constructor( @IFileService private readonly _fileService: IFileService, + @ITerminalService private readonly _terminalService: ITerminalService, @IInstantiationService readonly instantiationService: IInstantiationService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IElectronService electronService: IElectronService + @IRemoteAgentService readonly remoteAgentService: IRemoteAgentService, + @IElectronService readonly electronService: IElectronService ) { super(); - ipcRenderer.on('vscode:openFiles', (event: unknown, request: IOpenFileRequest) => this._onOpenFileRequest(request)); - this._register(electronService.onOSResume(() => this._onOsResume.fire())); + ipcRenderer.on('vscode:openFiles', (_: unknown, request: IOpenFileRequest) => this._onOpenFileRequest(request)); + this._register(electronService.onOSResume(() => this._onOsResume())); + + this._terminalService.setLinuxDistro(linuxDistro); + this._terminalService.setNativeWindowsDelegate({ + getWslPath: this._getWslPath.bind(this), + getWindowsBuildNumber: this._getWindowsBuildNumber.bind(this) + }); const connection = remoteAgentService.getConnection(); if (connection && connection.remoteAuthority) { @@ -46,18 +46,28 @@ export class TerminalNativeService extends Disposable implements ITerminalNative } } + private _onOsResume(): void { + const activeTab = this._terminalService.getActiveTab(); + if (!activeTab) { + return; + } + activeTab.terminalInstances.forEach(instance => instance.forceRedraw()); + } + private async _onOpenFileRequest(request: INativeOpenFileRequest): Promise { // if the request to open files is coming in from the integrated terminal (identified though // the termProgram variable) and we are instructed to wait for editors close, wait for the // marker file to get deleted and then focus back to the integrated terminal. if (request.termProgram === 'vscode' && request.filesToWait) { const waitMarkerFileUri = URI.revive(request.filesToWait.waitMarkerFileUri); - await this.whenFileDeleted(waitMarkerFileUri); - this._onRequestFocusActiveInstance.fire(); + await this._whenFileDeleted(waitMarkerFileUri); + + // Focus active terminal + this._terminalService.getActiveInstance()?.focus(); } } - public whenFileDeleted(path: URI): Promise { + private _whenFileDeleted(path: URI): Promise { // Complete when wait marker file is deleted return new Promise(resolve => { let running = false; @@ -80,7 +90,7 @@ export class TerminalNativeService extends Disposable implements ITerminalNative * Converts a path to a path on WSL using the wslpath utility. * @param path The original path. */ - public getWslPath(path: string): Promise { + private _getWslPath(path: string): Promise { if (getWindowsBuildNumber() < 17063) { throw new Error('wslpath does not exist on Windows build < 17063'); } @@ -92,7 +102,7 @@ export class TerminalNativeService extends Disposable implements ITerminalNative }); } - public getWindowsBuildNumber(): number { + private _getWindowsBuildNumber(): number { return getWindowsBuildNumber(); } } diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts index fad3e60d818..eed4382af0a 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts @@ -21,7 +21,7 @@ suite('Workbench - TerminalConfigHelper', () => { // const configurationService = new TestConfigurationService(); // configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); // configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: 'bar' } }); - // const configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!); + // const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!); // configHelper.panelContainer = fixture; // assert.equal(configHelper.getFont().fontFamily, 'bar', 'terminal.integrated.fontFamily should be selected over editor.fontFamily'); // }); @@ -30,7 +30,8 @@ suite('Workbench - TerminalConfigHelper', () => { const configurationService = new TestConfigurationService(); configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(LinuxDistro.Fedora, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper.setLinuxDistro(LinuxDistro.Fedora); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontFamily, '\'DejaVu Sans Mono\', monospace', 'Fedora should have its font overridden when terminal.integrated.fontFamily not set'); }); @@ -39,7 +40,8 @@ suite('Workbench - TerminalConfigHelper', () => { const configurationService = new TestConfigurationService(); configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper.setLinuxDistro(LinuxDistro.Ubuntu); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontFamily, '\'Ubuntu Mono\', monospace', 'Ubuntu should have its font overridden when terminal.integrated.fontFamily not set'); }); @@ -48,7 +50,7 @@ suite('Workbench - TerminalConfigHelper', () => { const configurationService = new TestConfigurationService(); configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontFamily, 'foo', 'editor.fontFamily should be the fallback when terminal.integrated.fontFamily not set'); }); @@ -66,7 +68,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: 10 } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 10, 'terminal.integrated.fontSize should be selected over editor.fontSize'); @@ -79,11 +81,12 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: 0 } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper.setLinuxDistro(LinuxDistro.Ubuntu); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 8, 'The minimum terminal font size (with adjustment) should be used when terminal.integrated.fontSize less than it'); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 6, 'The minimum terminal font size should be used when terminal.integrated.fontSize less than it'); @@ -96,7 +99,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: 1500 } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 25, 'The maximum terminal font size should be used when terminal.integrated.fontSize more than it'); @@ -109,11 +112,12 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: null } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper.setLinuxDistro(LinuxDistro.Ubuntu); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, EDITOR_FONT_DEFAULTS.fontSize + 2, 'The default editor font size (with adjustment) should be used when terminal.integrated.fontSize is not set'); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, EDITOR_FONT_DEFAULTS.fontSize, 'The default editor font size should be used when terminal.integrated.fontSize is not set'); }); @@ -131,7 +135,7 @@ suite('Workbench - TerminalConfigHelper', () => { lineHeight: 2 } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().lineHeight, 2, 'terminal.integrated.lineHeight should be selected over editor.lineHeight'); @@ -145,7 +149,7 @@ suite('Workbench - TerminalConfigHelper', () => { lineHeight: 0 } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().lineHeight, 1, 'editor.lineHeight should be 1 when terminal.integrated.lineHeight not set'); }); @@ -158,7 +162,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); }); @@ -170,7 +174,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontFamily: 'sans-serif' } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); }); @@ -182,7 +186,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontFamily: 'serif' } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); }); @@ -198,7 +202,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); }); @@ -214,7 +218,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); }); @@ -230,7 +234,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); }); diff --git a/src/vs/workbench/contrib/views/browser/treeView.ts b/src/vs/workbench/contrib/views/browser/treeView.ts index b13c36fadfb..a44f72ae44c 100644 --- a/src/vs/workbench/contrib/views/browser/treeView.ts +++ b/src/vs/workbench/contrib/views/browser/treeView.ts @@ -38,7 +38,7 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; -import { IHoverService, IHoverOptions } from 'vs/workbench/services/hover/browser/hover'; +import { IHoverService, IHoverOptions, IHoverTarget } from 'vs/workbench/services/hover/browser/hover'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; class Root implements ITreeItem { @@ -829,8 +829,13 @@ class TreeRenderer extends Disposable implements ITreeRenderer { } + }; + hoverOptions = { text: isString(tooltip) ? { value: tooltip } : tooltip, target }; } + (hoverOptions.target).x = e.x; hoverService.showHover(hoverOptions); } this.removeEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave); @@ -901,7 +906,7 @@ class Aligner extends Disposable { if (this._tree) { const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput(); if (this.hasIcon(parent)) { - return false; + return !!parent.children && parent.children.some(c => c.collapsibleState !== TreeItemCollapsibleState.None && !this.hasIcon(c)); } return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); } else { diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index d7135f2d671..84e12a399ba 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -141,7 +141,7 @@ export class HoverWidget extends Widget { // Get horizontal alignment and position let targetLeft = this._target.x !== undefined ? this._target.x : Math.min(...targetBounds.map(e => e.left)); if (targetLeft + this._hover.containerDomNode.clientWidth >= document.documentElement.clientWidth) { - this._x = document.documentElement.clientWidth; + this._x = document.documentElement.clientWidth - 1; this._hover.containerDomNode.classList.add('right-aligned'); } else { this._x = targetLeft; diff --git a/src/vs/workbench/services/hover/browser/media/hover.css b/src/vs/workbench/services/hover/browser/media/hover.css index 47d8ab484c6..6514844fd10 100644 --- a/src/vs/workbench/services/hover/browser/media/hover.css +++ b/src/vs/workbench/services/hover/browser/media/hover.css @@ -18,6 +18,11 @@ color: #3794ff; } +.monaco-workbench .workbench-hover.right-aligned { + /* The context view service wraps strangely when it's right up against the edge without this */ + left: 1px; +} + .monaco-workbench .workbench-hover.right-aligned .hover-row.status-bar .actions { flex-direction: row-reverse; } diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index ea89babdc73..f8c3d2107c2 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -692,7 +692,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor }); }); - this.getViewContainerModel(container).add(views.map(view => { return { viewDescriptor: view, collapsed: expandViews ? false : undefined }; })); + this.getViewContainerModel(container).add(views.map(view => { return { viewDescriptor: view, collapsed: expandViews ? false : undefined, visible: expandViews }; })); } private removeViews(container: ViewContainer, views: IViewDescriptor[]): void { diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts index 60c69da9c41..a8e7d7ccee7 100644 --- a/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -478,16 +478,16 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode if (state) { // set defaults if not set if (viewDescriptor.workspace) { - state.visibleWorkspace = isUndefinedOrNull(state.visibleWorkspace) ? !viewDescriptor.hideByDefault : state.visibleWorkspace; + state.visibleWorkspace = isUndefinedOrNull(addedViewDescriptorState.visible) ? (isUndefinedOrNull(state.visibleWorkspace) ? !viewDescriptor.hideByDefault : state.visibleWorkspace) : addedViewDescriptorState.visible; } else { - state.visibleGlobal = isUndefinedOrNull(state.visibleGlobal) ? !viewDescriptor.hideByDefault : state.visibleGlobal; + state.visibleGlobal = isUndefinedOrNull(addedViewDescriptorState.visible) ? (isUndefinedOrNull(state.visibleGlobal) ? !viewDescriptor.hideByDefault : state.visibleGlobal) : addedViewDescriptorState.visible; } state.collapsed = isUndefinedOrNull(addedViewDescriptorState.collapsed) ? (isUndefinedOrNull(state.collapsed) ? !!viewDescriptor.collapsed : state.collapsed) : addedViewDescriptorState.collapsed; } else { state = { active: false, - visibleGlobal: !viewDescriptor.hideByDefault, - visibleWorkspace: !viewDescriptor.hideByDefault, + visibleGlobal: isUndefinedOrNull(addedViewDescriptorState.visible) ? !viewDescriptor.hideByDefault : addedViewDescriptorState.visible, + visibleWorkspace: isUndefinedOrNull(addedViewDescriptorState.visible) ? !viewDescriptor.hideByDefault : addedViewDescriptorState.visible, collapsed: isUndefinedOrNull(addedViewDescriptorState.collapsed) ? !!viewDescriptor.collapsed : addedViewDescriptorState.collapsed, }; } diff --git a/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts b/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts index 93dead3f3df..ef2d1c5c0aa 100644 --- a/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts +++ b/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts @@ -35,7 +35,7 @@ suite('ExtHostDocumentData', () => { 'and this is line number two', //27 'it is followed by #3', //20 'and finished with the fourth.', //29 - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); }); test('readonly-ness', () => { @@ -55,7 +55,7 @@ suite('ExtHostDocumentData', () => { saved = uri; return Promise.resolve(true); } - }, URI.parse('foo:bar'), [], '\n', 'text', 1, true); + }, URI.parse('foo:bar'), [], '\n', 1, 'text', true); return data.document.save().then(() => { assert.equal(saved.toString(), 'foo:bar'); @@ -242,7 +242,7 @@ suite('ExtHostDocumentData', () => { test('getWordRangeAtPosition', () => { data = new ExtHostDocumentData(undefined!, URI.file(''), [ 'aaaa bbbb+cccc abc' - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); let range = data.document.getWordRangeAtPosition(new Position(0, 2))!; assert.equal(range.start.line, 0); @@ -276,7 +276,7 @@ suite('ExtHostDocumentData', () => { 'function() {', ' "far boo"', '}' - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); let range = data.document.getWordRangeAtPosition(new Position(0, 0), /\/\*.+\*\//); assert.equal(range, undefined); @@ -304,7 +304,7 @@ suite('ExtHostDocumentData', () => { data = new ExtHostDocumentData(undefined!, URI.file(''), [ perfData._$_$_expensive - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); let range = data.document.getWordRangeAtPosition(new Position(0, 1_177_170), regex)!; assert.equal(range, undefined); @@ -323,7 +323,7 @@ suite('ExtHostDocumentData', () => { data = new ExtHostDocumentData(undefined!, URI.file(''), [ line - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); let range = data.document.getWordRangeAtPosition(new Position(0, 27), regex)!; assert.equal(range.start.line, 0); @@ -387,7 +387,7 @@ suite('ExtHostDocumentData updates line mapping', () => { } function testLineMappingDirectionAfterEvents(lines: string[], eol: string, direction: AssertDocumentLineMappingDirection, e: IModelChangedEvent): void { - let myDocument = new ExtHostDocumentData(undefined!, URI.file(''), lines.slice(0), eol, 'text', 1, false); + let myDocument = new ExtHostDocumentData(undefined!, URI.file(''), lines.slice(0), eol, 1, 'text', false); assertDocumentLineMapping(myDocument, direction); myDocument.onEvents(e); diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index 98908bbb0eb..b8e9555996f 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -97,11 +97,13 @@ suite('NotebookCell#Document', function () { assert.ok(d1); assert.equal(d1.languageId, c1.language); assert.equal(d1.version, 1); + assert.ok(d1.notebook === notebook); const d2 = extHostDocuments.getDocument(c2.uri); assert.ok(d2); assert.equal(d2.languageId, c2.language); assert.equal(d2.version, 1); + assert.ok(d2.notebook === notebook); }); test('cell document goes when notebook closes', async function () { @@ -215,4 +217,10 @@ suite('NotebookCell#Document', function () { assert.equal(doc.isClosed, true); } }); + + test('cell document knows notebook', function () { + for (let cells of notebook.cells) { + assert.equal(cells.document.notebook === notebook, true); + } + }); }); diff --git a/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts b/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts index ddfaa9b86f6..dc844a5273a 100644 --- a/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts @@ -17,7 +17,7 @@ suite('ExtHostTextEditor', () => { let editor: ExtHostTextEditor; let doc = new ExtHostDocumentData(undefined!, URI.file(''), [ 'aaaa bbbb+cccc abc' - ], '\n', 'text', 1, false); + ], '\n', 1, 'text', false); setup(() => { editor = new ExtHostTextEditor('fake', null!, new NullLogService(), doc, [], { cursorStyle: 0, insertSpaces: true, lineNumbers: 1, tabSize: 4, indentSize: 4 }, [], 1);