diff --git a/.configurations/configuration.dsc.yaml b/.configurations/configuration.dsc.yaml index 255da69a5bb..780b1dfa959 100644 --- a/.configurations/configuration.dsc.yaml +++ b/.configurations/configuration.dsc.yaml @@ -12,11 +12,11 @@ properties: - resource: Microsoft.WinGet.DSC/WinGetPackage id: npm directives: - description: Install NodeJS version >=18.15.x and <19 + description: Install NodeJS version 20 allowPrerelease: true settings: id: OpenJS.NodeJS.LTS - version: "18.18.0" + version: "20.14.0" source: winget - resource: NpmDsc/NpmPackage id: yarn diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 31d67db5fac..bc30d7dbe3b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/typescript-node:18-bookworm +FROM mcr.microsoft.com/devcontainers/typescript-node:20-bookworm ADD install-vscode.sh /root/ RUN /root/install-vscode.sh diff --git a/.eslintplugin/code-no-static-self-ref.ts b/.eslintplugin/code-no-static-self-ref.ts new file mode 100644 index 00000000000..7c6e13032ae --- /dev/null +++ b/.eslintplugin/code-no-static-self-ref.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +/** + * WORKAROUND for https://github.com/evanw/esbuild/issues/3823 + */ +export = new class implements eslint.Rule.RuleModule { + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + function checkProperty(inNode: any) { + + const classDeclaration = context.getAncestors().find(node => node.type === 'ClassDeclaration'); + const propertyDefinition = inNode; + + if (!classDeclaration || !classDeclaration.id?.name) { + return; + } + + if (!propertyDefinition.value) { + return; + } + + const classCtor = classDeclaration.body.body.find(node => node.type === 'MethodDefinition' && node.kind === 'constructor') + + if (!classCtor) { + return; + } + + const name = classDeclaration.id.name; + const valueText = context.getSourceCode().getText(propertyDefinition.value) + + if (valueText.includes(name + '.')) { + + if (classCtor.value?.type === 'FunctionExpression' && !classCtor.value.params.find((param: any) => param.type === 'TSParameterProperty' && param.decorators?.length > 0)) { + return + } + + context.report({ + loc: propertyDefinition.value.loc, + message: `Static properties in decorated classes should not reference the class they are defined in. Use 'this' instead. This is a workaround for https://github.com/evanw/esbuild/issues/3823.` + }); + } + + } + + return { + 'PropertyDefinition[static=true]': checkProperty, + }; + } +}; diff --git a/.eslintrc.json b/.eslintrc.json index 8382822ac14..c39a66311e4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -141,6 +141,14 @@ ] } }, + { + "files": [ + "src/**/*.ts" + ], + "rules": { + "local/code-no-static-self-ref": "warn" + } + }, { "files": [ "src/vs/**/*.test.ts" @@ -151,21 +159,13 @@ { // Files should (only) be removed from the list they adopt the leak detector "exclude": [ - "src/vs/base/test/browser/browser.test.ts", - "src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts", - "src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts", "src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts", - "src/vs/editor/test/common/services/languageService.test.ts", - "src/vs/editor/test/node/classification/typescript.test.ts", "src/vs/platform/configuration/test/common/configuration.test.ts", - "src/vs/platform/extensions/test/common/extensionValidator.test.ts", "src/vs/platform/opener/test/common/opener.test.ts", "src/vs/platform/registry/test/common/platform.test.ts", - "src/vs/platform/remote/test/common/remoteHosts.test.ts", "src/vs/platform/workspace/test/common/workspace.test.ts", "src/vs/platform/workspaces/test/electron-main/workspaces.test.ts", "src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts", - "src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts", "src/vs/workbench/api/test/node/extHostTunnelService.test.ts", "src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts", "src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts", @@ -176,7 +176,6 @@ "src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts", "src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts", "src/vs/workbench/services/commands/test/common/commandService.test.ts", - "src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts", "src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts", "src/vs/workbench/test/browser/quickAccess.test.ts" ] @@ -317,6 +316,10 @@ "selector": "BinaryExpression[operator='instanceof'][right.name='MouseEvent']", "message": "Use DOM.isMouseEvent() to support multi-window scenarios." }, + { + "selector": "BinaryExpression[operator='instanceof'][right.name=/^HTML\\w+/]", + "message": "Use DOM.isHTMLElement() and related methods to support multi-window scenarios." + }, { "selector": "BinaryExpression[operator='instanceof'][right.name='KeyboardEvent']", "message": "Use DOM.isKeyboardEvent() to support multi-window scenarios." @@ -650,7 +653,6 @@ "events", "fs", "fs/promises", - "graceful-fs", "http", "https", "minimist", @@ -672,6 +674,7 @@ "vscode-regexpp", "vscode-textmate", "worker_threads", + "@xterm/addon-clipboard", "@xterm/addon-image", "@xterm/addon-search", "@xterm/addon-serialize", @@ -1021,11 +1024,7 @@ ] }, { - "target": "src/vs/workbench/{workbench.desktop.main.nls.js,workbench.web.main.nls.js}", - "restrictions": [] - }, - { - "target": "src/vs/{loader.d.ts,css.ts,css.build.ts,monaco.d.ts,nls.ts,nls.build.ts,nls.mock.ts}", + "target": "src/vs/{loader.d.ts,css.ts,css.build.ts,monaco.d.ts,nls.ts}", "restrictions": [] }, { @@ -1099,7 +1098,9 @@ "local/code-no-runtime-import": [ "error", { - "src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts": ["**/*"] + "src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts": [ + "**/*" + ] } ] } diff --git a/.github/classifier.json b/.github/classifier.json index 44514039e1e..d0a2c778997 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -32,7 +32,7 @@ "debug": {"assign": ["roblourens"]}, "debug-disassembly": {"assign": []}, "dialogs": {"assign": ["sbatten"]}, - "diff-editor": {"assign": ["alexdima"]}, + "diff-editor": {"assign": ["hediet"]}, "dropdown": {"assign": ["lramos15"]}, "editor-api": {"assign": ["alexdima"]}, "editor-autoclosing": {"assign": ["alexdima"]}, @@ -116,7 +116,7 @@ "json": {"assign": ["aeschli"]}, "json-sorting": {"assign": ["aiday-mar"]}, "keybindings": {"assign": ["ulugbekna"]}, - "keybindings-editor": {"assign": ["sandy081"]}, + "keybindings-editor": {"assign": ["ulugbekna"]}, "keyboard-layout": {"assign": ["ulugbekna"]}, "L10N": {"assign": ["TylerLeonhardt", "csigs"]}, "l10n-platform": {"assign": ["TylerLeonhardt"]}, diff --git a/.github/workflows/deep-classifier-assign-monitor.yml b/.github/workflows/deep-classifier-assign-monitor.yml index cfd9abc374a..a61f9cfb137 100644 --- a/.github/workflows/deep-classifier-assign-monitor.yml +++ b/.github/workflows/deep-classifier-assign-monitor.yml @@ -21,4 +21,3 @@ jobs: with: botName: VSCodeTriageBot token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} diff --git a/.github/workflows/deep-classifier-runner.yml b/.github/workflows/deep-classifier-runner.yml index 81fd3516751..7145de06db5 100644 --- a/.github/workflows/deep-classifier-runner.yml +++ b/.github/workflows/deep-classifier-runner.yml @@ -40,9 +40,7 @@ jobs: excludeLabels: feature-request|testplan-item configPath: classifier blobContainerName: vscode-issue-classifier - blobStorageKey: ${{secrets.AZURE_BLOB_STORAGE_CONNECTION_STRING}} token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - name: Set up Python 3.7 uses: actions/setup-python@v5 with: diff --git a/.github/workflows/deep-classifier-scraper.yml b/.github/workflows/deep-classifier-scraper.yml index e21061549d9..e663372fad0 100644 --- a/.github/workflows/deep-classifier-scraper.yml +++ b/.github/workflows/deep-classifier-scraper.yml @@ -1,4 +1,9 @@ name: "Deep Classifier: Scraper" + +permissions: + id-token: write + contents: read + on: schedule: - cron: 0 0 15 * * # 15th of the month @@ -9,7 +14,13 @@ on: jobs: main: runs-on: ubuntu-latest + environment: main steps: + - uses: azure/login@v2 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + allow-no-subscriptions: true - name: Checkout Actions uses: actions/checkout@v4 with: @@ -25,6 +36,4 @@ jobs: uses: ./actions/classifier-deep/train/fetch-issues with: blobContainerName: vscode-issue-classifier - blobStorageKey: ${{secrets.AZURE_BLOB_STORAGE_CONNECTION_STRING}} token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} diff --git a/.github/workflows/deep-classifier-unassign-monitor.yml b/.github/workflows/deep-classifier-unassign-monitor.yml index d0e14e936c2..52ac0d3ddcd 100644 --- a/.github/workflows/deep-classifier-unassign-monitor.yml +++ b/.github/workflows/deep-classifier-unassign-monitor.yml @@ -21,4 +21,3 @@ jobs: with: botName: VSCodeTriageBot token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index 5860349a437..ef775ce8fdf 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -20,9 +20,10 @@ jobs: - name: Run Locker uses: ./actions/locker with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} daysSinceClose: 45 - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} daysSinceUpdate: 3 ignoredLabel: "*out-of-scope,accessibility" ignoreLabelUntil: "author-verification-requested" + ignoredMilestones: "Backlog Candidates" labelUntil: "verified" diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml index 361ac11b946..2a26794c6b0 100644 --- a/.github/workflows/on-open.yml +++ b/.github/workflows/on-open.yml @@ -16,7 +16,13 @@ jobs: - name: Install Actions run: npm install --production --prefix ./actions + - name: Check for Validity + uses: ./actions/validity-checker + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + - name: Run CopyCat (VSCodeTriageBot/testissues) + if: github.event.issue.user.login != 'ghost' uses: ./actions/copycat with: appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} @@ -25,6 +31,7 @@ jobs: repo: testissues - name: Run New Release + if: github.event.issue.user.login != 'ghost' uses: ./actions/new-release with: label: new release @@ -36,6 +43,7 @@ jobs: days: 5 - name: Run Clipboard Labeler + if: github.event.issue.user.login != 'ghost' uses: ./actions/regex-labeler with: appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} @@ -44,6 +52,7 @@ jobs: comment: "It looks like you're using the VS Code Issue Reporter but did not paste the text generated into the created issue. We've closed this issue, please open a new one containing the text we placed in your clipboard.\n\nHappy Coding!" - name: Run Clipboard Labeler (Chinese) + if: github.event.issue.user.login != 'ghost' uses: ./actions/regex-labeler with: appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} @@ -53,6 +62,7 @@ jobs: # source of truth in ./english-please.yml - name: Run English Please + if: github.event.issue.user.login != 'ghost' uses: ./actions/english-please with: token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} @@ -64,6 +74,7 @@ jobs: translatorRequestedLabelColor: "c29cff" # source of truth in ./test-plan-item-validator.yml - name: Run Test Plan Item Validator + if: github.event.issue.user.login != 'ghost' uses: ./actions/test-plan-item-validator with: token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.github/workflows/on-reopen.yml b/.github/workflows/on-reopen.yml new file mode 100644 index 00000000000..d29de326c53 --- /dev/null +++ b/.github/workflows/on-reopen.yml @@ -0,0 +1,22 @@ +name: On Reopen +on: + issues: + types: [reopened] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v4 + with: + repository: "microsoft/vscode-github-triage-actions" + ref: stable + path: ./actions + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: Check for Validity + uses: ./actions/validity-checker + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts index 7280782c10a..3fff7c5b637 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts @@ -5,7 +5,7 @@ import { IstanbulCoverageContext } from 'istanbul-to-vscode'; import * as vscode from 'vscode'; -import { SourceLocationMapper, SourceMapStore } from './testOutputScanner'; +import { SearchStrategy, SourceLocationMapper, SourceMapStore } from './testOutputScanner'; import { IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling'; export const istanbulCoverageContext = new IstanbulCoverageContext(); @@ -18,7 +18,7 @@ export const istanbulCoverageContext = new IstanbulCoverageContext(); export class PerTestCoverageTracker { private readonly scripts = new Map(); - constructor(private readonly maps: SourceMapStore) {} + constructor(private readonly maps: SourceMapStore) { } public add(coverage: IScriptCoverage, test?: vscode.TestItem) { const script = this.scripts.get(coverage.scriptId); @@ -71,11 +71,7 @@ class Script { public async report(run: vscode.TestRun) { const mapper = await this.maps.getSourceLocationMapper(this.uri.toString()); const originalUri = (await this.maps.getSourceFile(this.uri.toString())) || this.uri; - - run.addCoverage(this.overall.report(originalUri, this.converter, mapper)); - for (const [test, projection] of this.perItem) { - run.addCoverage(projection.report(originalUri, this.converter, mapper, test)); - } + run.addCoverage(this.overall.report(originalUri, this.converter, mapper, this.perItem)); } } @@ -88,6 +84,43 @@ class ScriptCoverageTracker { } } + public *toDetails( + uri: vscode.Uri, + convert: OffsetToPosition, + mapper: SourceLocationMapper | undefined, + ) { + for (const range of this.coverage) { + if (range.start === range.end) { + continue; + } + + const startCov = convert.toLineColumn(range.start); + let start = new vscode.Position(startCov.line, startCov.column); + + const endCov = convert.toLineColumn(range.end); + let end = new vscode.Position(endCov.line, endCov.column); + if (mapper) { + const startMap = mapper(start.line, start.character, SearchStrategy.FirstAfter); + const endMap = startMap && mapper(end.line, end.character, SearchStrategy.FirstBefore); + if (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase()) { + continue; + } + start = startMap.range.start; + end = endMap.range.end; + } + + for (let i = start.line; i <= end.line; i++) { + yield new vscode.StatementCoverage( + range.covered, + new vscode.Range( + new vscode.Position(i, i === start.line ? start.character : 0), + new vscode.Position(i, i === end.line ? end.character : Number.MAX_SAFE_INTEGER) + ) + ); + } + } + } + /** * Generates the script's coverage for the test run. * @@ -98,53 +131,27 @@ class ScriptCoverageTracker { uri: vscode.Uri, convert: OffsetToPosition, mapper: SourceLocationMapper | undefined, - item?: vscode.TestItem + items: Map, ): V8CoverageFile { - const file = new V8CoverageFile(uri, item); - - for (const range of this.coverage) { - if (range.start === range.end) { - continue; - } - - const startCov = convert.toLineColumn(range.start); - let start = new vscode.Position(startCov.line, startCov.column); - - const endCov = convert.toLineColumn(range.end); - let end = new vscode.Position(endCov.line, endCov.column); - if (mapper) { - const startMap = mapper(start.line, start.character); - const endMap = startMap && mapper(end.line, end.character); - if (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase()) { - continue; - } - start = startMap.range.start; - end = endMap.range.end; - } - - for (let i = start.line; i <= end.line; i++) { - file.add( - new vscode.StatementCoverage( - range.covered, - new vscode.Range( - new vscode.Position(i, i === start.line ? start.character : 0), - new vscode.Position(i, i === end.line ? end.character : Number.MAX_SAFE_INTEGER) - ) - ) - ); - } + const file = new V8CoverageFile(uri, items, convert, mapper); + for (const detail of this.toDetails(uri, convert, mapper)) { + file.add(detail); } return file; } } -export class V8CoverageFile extends vscode.FileCoverage { +export class V8CoverageFile extends vscode.FileCoverage2 { public details: vscode.StatementCoverage[] = []; - constructor(uri: vscode.Uri, item?: vscode.TestItem) { - super(uri, { covered: 0, total: 0 }); - (this as vscode.FileCoverage2).testItem = item; + constructor( + uri: vscode.Uri, + private readonly perTest: Map, + private readonly convert: OffsetToPosition, + private readonly mapper: SourceLocationMapper | undefined, + ) { + super(uri, { covered: 0, total: 0 }, undefined, undefined, [...perTest.keys()]); } public add(detail: vscode.StatementCoverage) { @@ -154,4 +161,9 @@ export class V8CoverageFile extends vscode.FileCoverage { this.statementCoverage.covered++; } } + + public testDetails(test: vscode.TestItem): vscode.FileCoverageDetail[] { + const t = this.perTest.get(test); + return t ? [...t.toDetails(this.uri, this.convert, this.mapper)] : []; + } } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index 960dbcf634e..491f67ee300 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -44,7 +44,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.tests.registerTestFollowupProvider({ async provideFollowup(_result, test, taskIndex, messageIndex, _token) { return [{ - title: '$(sparkle) Ask copilot for help', + title: '$(sparkle) Fix with Copilot', command: 'github.copilot.tests.fixTestFailure', arguments: [{ source: 'peekFollowup', test, message: test.taskStates[taskIndex].messages[messageIndex] }] }]; @@ -119,7 +119,7 @@ export async function activate(context: vscode.ExtensionContext) { map, task, kind === vscode.TestRunProfileKind.Debug - ? await runner.debug(currentArgs, req.include) + ? await runner.debug(task, currentArgs, req.include) : await runner.run(currentArgs, req.include), coverageDir, cancellationToken @@ -196,13 +196,8 @@ export async function activate(context: vscode.ExtensionContext) { true ); - coverage.loadDetailedCoverage = async (_run, coverage) => { - if (coverage instanceof V8CoverageFile) { - return coverage.details; - } - - return []; - }; + coverage.loadDetailedCoverage = async (_run, coverage) => coverage instanceof V8CoverageFile ? coverage.details : []; + coverage.loadDetailedCoverageForTest = async (_run, coverage, test) => coverage instanceof V8CoverageFile ? coverage.testDetails(test) : []; for (const [name, arg] of browserArgs) { const cfg = ctrl.createRunProfile( diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index 74013dcd561..b5a448aaba1 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -424,36 +424,62 @@ const tryMakeMarkdown = (message: string) => { const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m; const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const; -export type SourceLocationMapper = (line: number, col: number) => vscode.Location | undefined; +export const enum SearchStrategy { + FirstBefore = -1, + FirstAfter = 1, +} + +export type SourceLocationMapper = (line: number, col: number, strategy: SearchStrategy) => vscode.Location | undefined; export class SourceMapStore { private readonly cache = new Map>(); - async getSourceLocationMapper(fileUri: string) { + async getSourceLocationMapper(fileUri: string): Promise { const sourceMap = await this.loadSourceMap(fileUri); - return (line: number, col: number) => { + return (line, col, strategy) => { if (!sourceMap) { return undefined; } - let smLine = line + 1; + // 1. Look for the ideal position on this line if it exists + const idealPosition = originalPositionFor(sourceMap, { column: col, line: line + 1, bias: SearchStrategy.FirstAfter ? GREATEST_LOWER_BOUND : LEAST_UPPER_BOUND }); + if (idealPosition.line !== null && idealPosition.column !== null && idealPosition.source !== null) { + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, idealPosition.source), + new vscode.Position(idealPosition.line - 1, idealPosition.column) + ); + } - // if the range is after the end of mappings, adjust it to the last mapped line + // Otherwise get the first/last valid mapping on another line. const decoded = decodedMappings(sourceMap); - if (decoded.length <= line) { - smLine = decoded.length; // base 1, no -1 needed - col = Number.MAX_SAFE_INTEGER; + const enum MapField { + COLUMN = 0, + SOURCES_INDEX = 1, + SOURCE_LINE = 2, + SOURCE_COLUMN = 3, } - for (const bias of sourceMapBiases) { - const position = originalPositionFor(sourceMap, { column: col, line: smLine, bias }); - if (position.line !== null && position.column !== null && position.source !== null) { - return new vscode.Location( - this.completeSourceMapUrl(sourceMap, position.source), - new vscode.Position(position.line - 1, position.column) - ); + do { + line += strategy; + const segments = decoded[line]; + if (!segments?.length) { + continue; } - } + + const index = strategy === SearchStrategy.FirstBefore + ? findLastIndex(segments, s => s.length !== 1) + : segments.findIndex(s => s.length !== 1); + const segment = segments[index]; + + if (!segment || segment.length === 1) { + continue; + } + + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, sourceMap.sources[segment[MapField.SOURCES_INDEX]]!), + new vscode.Position(segment[MapField.SOURCE_LINE] - 1, segment[MapField.SOURCE_COLUMN]) + ); + } while (strategy === SearchStrategy.FirstBefore ? line > 0 : line < decoded.length); return undefined; }; @@ -461,7 +487,31 @@ export class SourceMapStore { /** Gets an original location from a base 0 line and column */ async getSourceLocation(fileUri: string, line: number, col = 0) { - return this.getSourceLocationMapper(fileUri).then(m => m(line, col)); + const sourceMap = await this.loadSourceMap(fileUri); + if (!sourceMap) { + return undefined; + } + + let smLine = line + 1; + + // if the range is after the end of mappings, adjust it to the last mapped line + const decoded = decodedMappings(sourceMap); + if (decoded.length <= line) { + smLine = decoded.length; // base 1, no -1 needed + col = Number.MAX_SAFE_INTEGER; + } + + for (const bias of sourceMapBiases) { + const position = originalPositionFor(sourceMap, { column: col, line: smLine, bias }); + if (position.line !== null && position.column !== null && position.source !== null) { + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, position.source), + new vscode.Position(position.line - 1, position.column) + ); + } + } + + return undefined; } async getSourceFile(compiledUri: string) { @@ -602,3 +652,13 @@ async function deriveSourceLocation(store: SourceMapStore, parts: RegExpMatchArr const [, fileUri, line, col] = parts; return store.getSourceLocation(fileUri, Number(line) - 1, Number(col)); } + +function findLastIndex(arr: T[], predicate: (value: T) => boolean) { + for (let i = arr.length - 1; i >= 0; i--) { + if (predicate(arr[i])) { + return i; + } + } + + return -1; +} \ No newline at end of file diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts index 5d73928aed5..954b847f4a8 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts @@ -24,7 +24,7 @@ const ATTACH_CONFIG_NAME = 'Attach to VS Code'; const DEBUG_TYPE = 'pwa-chrome'; export abstract class VSCodeTestRunner { - constructor(protected readonly repoLocation: vscode.WorkspaceFolder) {} + constructor(protected readonly repoLocation: vscode.WorkspaceFolder) { } public async run(baseArgs: ReadonlyArray, filter?: ReadonlyArray) { const args = this.prepareArguments(baseArgs, filter); @@ -37,7 +37,7 @@ export abstract class VSCodeTestRunner { return new TestOutputScanner(cp, args); } - public async debug(baseArgs: ReadonlyArray, filter?: ReadonlyArray) { + public async debug(testRun: vscode.TestRun, baseArgs: ReadonlyArray, filter?: ReadonlyArray) { const port = await this.findOpenPort(); const baseConfiguration = vscode.workspace .getConfiguration('launch', this.repoLocation) @@ -95,7 +95,7 @@ export abstract class VSCodeTestRunner { }, }); - vscode.debug.startDebugging(this.repoLocation, { ...baseConfiguration, port }); + vscode.debug.startDebugging(this.repoLocation, { ...baseConfiguration, port }, { testRun }); let exited = false; let rootSession: vscode.DebugSession | undefined; @@ -163,18 +163,40 @@ export abstract class VSCodeTestRunner { path.relative(data.workspaceFolder.uri.fsPath, data.uri.fsPath).replace(/\\/g, '/') ); + const itemDatas = filter.map(f => itemData.get(f)); + /** If true, we have to be careful with greps, as a grep for one test file affects the run of the other test file. */ + const hasBothTestCaseOrTestSuiteAndTestFileFilters = + itemDatas.some(d => (d instanceof TestCase) || (d instanceof TestSuite)) && + itemDatas.some(d => d instanceof TestFile); + + function addTestCaseOrSuite(data: TestCase | TestSuite, test: vscode.TestItem): void { + grepRe.push(escapeRe(data.fullName) + (data instanceof TestCase ? '$' : ' ')); + for (let p = test.parent; p; p = p.parent) { + const parentData = itemData.get(p); + if (parentData instanceof TestFile) { + addTestFileRunPath(parentData); + } + } + } + for (const test of filter) { const data = itemData.get(test); if (data instanceof TestCase || data instanceof TestSuite) { - grepRe.push(escapeRe(data.fullName) + (data instanceof TestCase ? '$' : ' ')); - for (let p = test.parent; p; p = p.parent) { - const parentData = itemData.get(p); - if (parentData instanceof TestFile) { - addTestFileRunPath(parentData); + addTestCaseOrSuite(data, test); + } else if (data instanceof TestFile) { + if (!hasBothTestCaseOrTestSuiteAndTestFileFilters) { + addTestFileRunPath(data); + } else { + // We add all the items individually so they get their own grep expressions. + for (const [_id, nestedTest] of test.children) { + const childData = itemData.get(nestedTest); + if (childData instanceof TestCase || childData instanceof TestSuite) { + addTestCaseOrSuite(childData, nestedTest); + } else { + console.error('Unexpected test item in test file', nestedTest.id, nestedTest.label); + } } } - } else if (data instanceof TestFile) { - addTestFileRunPath(data); } } @@ -303,5 +325,5 @@ export const PlatformTestRunner = process.platform === 'win32' ? WindowsTestRunner : process.platform === 'darwin' - ? DarwinTestRunner - : PosixTestRunner; + ? DarwinTestRunner + : PosixTestRunner; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json index 0183a2ff57e..9725e14041e 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json @@ -11,6 +11,6 @@ "src/**/*", "../../../src/vscode-dts/vscode.d.ts", "../../../src/vscode-dts/vscode.proposed.testObserver.d.ts", - "../../../src/vscode-dts/vscode.proposed.attributableCoverage.d.ts", + "../../../src/vscode-dts/vscode.proposed.attributableCoverage.d.ts" ] } diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 2ac7d699582..957ce5a9ee0 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"May 2024\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"June 2024\"" }, { "kind": 1, diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index d7836922bad..e1c0a9fce02 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"May 2024\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\r\n\r\n$MILESTONE=milestone:\"June 2024\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 3bf56fffce4..0b260270ed7 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"May 2024\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"June 2024\"\n\n$MINE=assignee:@me" }, { "kind": 1, @@ -157,7 +157,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed sort:updated-asc label:bug -label:unreleased -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:*out-of-scope -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:andreamah -author:bamurtaugh -author:bpasero -author:chrisdias -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:gregvanl -author:hediet -author:isidorn -author:joaomoreno -author:joyceerhl -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:tanhakabir -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:paulacamargo25 -author:ulugbekna -author:aiday-mar -author:daviddossett -author:bhavyaus -author:justschen -author:benibenj -author:luabud" + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed sort:updated-asc label:bug -label:unreleased -label:verified -label:z-author-verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:*out-of-scope -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:andreamah -author:bamurtaugh -author:bpasero -author:chrisdias -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:gregvanl -author:hediet -author:isidorn -author:joaomoreno -author:joyceerhl -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:tanhakabir -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:paulacamargo25 -author:ulugbekna -author:aiday-mar -author:daviddossett -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 091c6e8a886..27fc3c2ecb5 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"May 2024\"\n" + "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"June 2024\"\n" }, { "kind": 1, @@ -102,7 +102,7 @@ { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode assignee:@me is:open type:issue -label:\"info-needed\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:code-lens -label:command-center -label:comments -label:config -label:context-keys -label:custom-editors -label:debug -label:debug-console -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor-api -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-RTL -label:editor-scrollbar -label:editor-sorting -label:editor-sticky-scroll -label:editor-sticky-scroll-decorations -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet-parse -label:extension-activation -label:extension-host -label:extension-prerelease -label:extension-recommendations -label:extension-signature -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-nesting -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:github-repositories -label:gpu -label:grammar -label:grid-widget -label:icon-brand -label:icons-product -label:icons-widget -label:inlay-hints -label:inline-chat -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-playground -label:interactive-window -label:javascript -label:json -label:json-sorting -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:L10N -label:l10n-platform -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list-widget -label:live-preview -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:merge-editor -label:merge-editor-workbench -label:monaco-editor -label:multi-monitor -label:native-file-dialog -label:network -label:notebook -label:notebook-accessibility -label:notebook-api -label:notebook-builtin-renderers -label:notebook-cell-editor -label:notebook-celltoolbar -label:notebook-clipboard -label:notebook-commands -label:notebook-commenting -label:notebook-debugging -label:notebook-diff -label:notebook-dnd -label:notebook-execution -label:notebook-find -label:notebook-folding -label:notebook-getting-started -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-kernel-picker -label:notebook-language -label:notebook-layout -label:notebook-markdown -label:notebook-math -label:notebook-minimap -label:notebook-multiselect -label:notebook-output -label:notebook-perf -label:notebook-remote -label:notebook-rendering -label:notebook-serialization -label:notebook-serverless-web -label:notebook-statusbar -label:notebook-sticky-scroll -label:notebook-toc-outline -label:notebook-undo-redo -label:notebook-variables -label:notebook-workbench-integration -label:notebook-workflow -label:open-editors -label:opener -label:outline -label:output -label:packaging -label:panel-chat -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:quickpick-chat -label:references-viewlet -label:release-notes -label:remote -label:remote-connection -label:remote-desktop -label:remote-explorer -label:remote-tunnel -label:rename -label:runCommands -label:sandbox -label:sash-widget -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:server -label:settings-editor -label:settings-search -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview-widget -label:ssh -label:suggest -label:system-context-menu -label:table-widget -label:tasks -label:telemetry -label:terminal -label:terminal-accessibility -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-find -label:terminal-input -label:terminal-layout -label:terminal-links -label:terminal-local-echo -label:terminal-persistence -label:terminal-process -label:terminal-profiles -label:terminal-quick-fix -label:terminal-rendering -label:terminal-shell-bash -label:terminal-shell-cmd -label:terminal-shell-fish -label:terminal-shell-git-bash -label:terminal-shell-integration -label:terminal-shell-pwsh -label:terminal-shell-zsh -label:terminal-tabs -label:terminal-winpty -label:testing -label:themes -label:timeline -label:timeline-git -label:timeline-local-history -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typescript -label:unc -label:undo-redo -label:unicode-highlight -label:uri -label:user-profiles -label:ux -label:variable-resolving -label:VIM -label:virtual-documents -label:virtual-workspaces -label:vscode-website -label:vscode.dev -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-auxwindow -label:workbench-banner -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-feedback -label:workbench-fonts -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-voice -label:workbench-welcome -label:workbench-window -label:workbench-workspace -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom -label:error-list -label:winget" + "value": "repo:microsoft/vscode assignee:@me is:open type:issue -label:\"info-needed\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:code-lens -label:command-center -label:comments -label:config -label:context-keys -label:custom-editors -label:debug -label:debug-console -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor-api -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-RTL -label:editor-scrollbar -label:editor-sorting -label:editor-sticky-scroll -label:editor-sticky-scroll-decorations -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet-parse -label:extension-activation -label:extension-host -label:extension-prerelease -label:extension-recommendations -label:extension-signature -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-nesting -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:github-repositories -label:gpu -label:grammar -label:grid-widget -label:icon-brand -label:icons-product -label:icons-widget -label:inlay-hints -label:inline-chat -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-playground -label:interactive-window -label:javascript -label:json -label:json-sorting -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:L10N -label:l10n-platform -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list-widget -label:live-preview -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:merge-editor -label:merge-editor-workbench -label:monaco-editor -label:multi-monitor -label:native-file-dialog -label:network -label:notebook -label:notebook-accessibility -label:notebook-api -label:notebook-builtin-renderers -label:notebook-cell-editor -label:notebook-celltoolbar -label:notebook-clipboard -label:notebook-code-actions -label:notebook-commands -label:notebook-commenting -label:notebook-debugging -label:notebook-diff -label:notebook-dnd -label:notebook-execution -label:notebook-find -label:notebook-folding -label:notebook-format -label:notebook-getting-started -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-kernel-picker -label:notebook-language -label:notebook-layout -label:notebook-markdown -label:notebook-minimap -label:notebook-multiselect -label:notebook-output -label:notebook-perf -label:notebook-remote -label:notebook-rendering -label:notebook-serialization -label:notebook-statusbar -label:notebook-sticky-scroll -label:notebook-toc-outline -label:notebook-undo-redo -label:notebook-variables -label:notebook-workbench-integration -label:notebook-workflow -label:open-editors -label:opener -label:outline -label:output -label:packaging -label:panel-chat -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:quickpick-chat -label:references-viewlet -label:release-notes -label:remote -label:remote-connection -label:remote-desktop -label:remote-explorer -label:remote-tunnel -label:rename -label:runCommands -label:sandbox -label:sash-widget -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:server -label:settings-editor -label:settings-search -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview-widget -label:ssh -label:suggest -label:system-context-menu -label:table-widget -label:tasks -label:telemetry -label:terminal -label:terminal-accessibility -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-find -label:terminal-input -label:terminal-layout -label:terminal-links -label:terminal-local-echo -label:terminal-persistence -label:terminal-process -label:terminal-profiles -label:terminal-quick-fix -label:terminal-rendering -label:terminal-shell-bash -label:terminal-shell-cmd -label:terminal-shell-fish -label:terminal-shell-git-bash -label:terminal-shell-integration -label:terminal-shell-pwsh -label:terminal-shell-zsh -label:terminal-tabs -label:testing -label:themes -label:timeline -label:timeline-git -label:timeline-local-history -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typescript -label:unc -label:undo-redo -label:unicode-highlight -label:uri -label:user-profiles -label:ux -label:variable-resolving -label:VIM -label:virtual-documents -label:virtual-workspaces -label:vscode-website -label:vscode.dev -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-auxwindow -label:workbench-banner -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-fonts -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-voice -label:workbench-welcome -label:workbench-window -label:workbench-workspace -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom -label:error-list -label:winget" }, { "kind": 1, diff --git a/.vscode/settings.json b/.vscode/settings.json index b8c63e5bbe0..9255a781bd4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,8 +40,6 @@ "**/Cargo.lock": true, "src/vs/workbench/workbench.web.main.css": true, "src/vs/workbench/workbench.desktop.main.css": true, - "src/vs/workbench/workbench.desktop.main.nls.js": true, - "src/vs/workbench/workbench.web.main.nls.js": true, "build/**/*.js": true, "out/**": true, "out-build/**": true, @@ -162,14 +160,13 @@ "@xterm/headless", "node-pty", "vscode-notebook-renderer", - "src/vs/workbench/workbench.web.main.ts", - "src/vs/workbench/api/common/extHostTypes.ts" + "src/vs/workbench/workbench.web.main.ts" ], "[github-issues]": { "editor.wordWrap": "on" }, "css.format.spaceAroundSelectorSeparator": true, "inlineChat.mode": "live", - "typescript.enablePromptUseWorkspaceTsdk": true, - "typescript.tsserver.experimental.useVsCodeWatcher": true + "inlineChat.experimental.textButtons": true, + "typescript.enablePromptUseWorkspaceTsdk": true } diff --git a/.yarnrc b/.yarnrc index b40fb7e7f58..b153fa4724f 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" target "29.4.0" -ms_build_id "9593362" +ms_build_id "9728852" runtime "electron" build_from_source "true" diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 60c0c7e445e..fe3cf5c4a9b 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -517,7 +517,7 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- -go-syntax 0.6.5 - MIT +go-syntax 0.6.8 - MIT https://github.com/worlpaker/go-syntax MIT License @@ -777,7 +777,7 @@ SOFTWARE. --------------------------------------------------------- -jeff-hykin/better-shell-syntax 1.8.3 - MIT +jeff-hykin/better-shell-syntax 1.8.7 - MIT https://github.com/jeff-hykin/better-shell-syntax MIT License @@ -833,7 +833,7 @@ SOFTWARE. --------------------------------------------------------- -jlelong/vscode-latex-basics 1.7.0 - MIT +jlelong/vscode-latex-basics 1.9.0 - MIT https://github.com/jlelong/vscode-latex-basics Copyright (c) vscode-latex-basics authors diff --git a/build/.cachesalt b/build/.cachesalt index a454f1220da..d7d415d3213 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2024-05-16T14:24:05.381Z +2024-07-04T16:31:11.121Z diff --git a/build/.webignore b/build/.webignore index 88fe96f5cc1..15935edce8a 100644 --- a/build/.webignore +++ b/build/.webignore @@ -20,6 +20,9 @@ vscode-textmate/webpack.config.js @xterm/xterm/src/** +@xterm/addon-clipboard/src/** +@xterm/addon-clipboard/out/** + @xterm/addon-image/src/** @xterm/addon-image/out/** diff --git a/build/azure-pipelines/cli/cli-compile.yml b/build/azure-pipelines/cli/cli-compile.yml index 267682f7f6d..e77ba78a999 100644 --- a/build/azure-pipelines/cli/cli-compile.yml +++ b/build/azure-pipelines/cli/cli-compile.yml @@ -49,16 +49,22 @@ steps: export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc" export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" export CC_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc --sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export PKG_CONFIG_LIBDIR_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu/pkgconfig:$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/share/pkgconfig" + export PKG_CONFIG_SYSROOT_DIR_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" export OBJDUMP="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/bin/objdump" elif [ "$SYSROOT_ARCH" == "amd64" ]; then export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc" export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -C link-arg=-L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu" export CC_x86_64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export PKG_CONFIG_LIBDIR_x86_64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu/pkgconfig:$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/share/pkgconfig" + export PKG_CONFIG_SYSROOT_DIR_x86_64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" export OBJDUMP="$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/bin/objdump" elif [ "$SYSROOT_ARCH" == "armhf" ]; then export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER="$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc" export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_RUSTFLAGS="-C link-arg=--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" export CC_armv7_unknown_linux_gnueabihf="$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc --sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export PKG_CONFIG_LIBDIR_armv7_unknown_linux_gnueabihf="$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-rpi-linux-gnueabihf/pkgconfig:$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/share/pkgconfig" + export PKG_CONFIG_SYSROOT_DIR_armv7_unknown_linux_gnueabihf="$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" export OBJDUMP="$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/bin/objdump" fi fi @@ -78,7 +84,7 @@ steps: fi done < <("$OBJDUMP" -T "$PWD/target/${{ parameters.VSCODE_CLI_TARGET }}/release/code") if [[ "$glibc_version" != "2.17" ]]; then - echo "Error: binary has dependency on GLIBC > 2.17" + echo "Error: binary has dependency on GLIBC > 2.17, found $glibc_version" exit 1 fi fi diff --git a/build/azure-pipelines/darwin/helper-plugin-entitlements.plist b/build/azure-pipelines/darwin/helper-plugin-entitlements.plist index 1cc1a152c74..48f7bf5cece 100644 --- a/build/azure-pipelines/darwin/helper-plugin-entitlements.plist +++ b/build/azure-pipelines/darwin/helper-plugin-entitlements.plist @@ -6,8 +6,6 @@ com.apple.security.cs.allow-unsigned-executable-memory - com.apple.security.cs.allow-dyld-environment-variables - com.apple.security.cs.disable-library-validation diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 8b4bda1c6a2..11f69d735ac 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -78,6 +78,14 @@ steps: - script: | set -e + # Refs https://github.com/microsoft/vscode/issues/219893#issuecomment-2209313109 + sudo xcode-select --switch /Applications/Xcode_15.2.app + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Switch to Xcode >= 15.1 + + - script: | + set -e + c++ --version python3 -m pip install setuptools for i in {1..5}; do # try 5 times diff --git a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml index dc8424f26ee..921bf2a9370 100644 --- a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml +++ b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml @@ -133,14 +133,6 @@ steps: VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-arm32v7 displayName: Install dependencies - - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: - - script: | - set -e - EXPECTED_GLIBC_VERSION="2.17" \ - EXPECTED_GLIBCXX_VERSION="3.4.19" \ - ./build/azure-pipelines/linux/verify-glibc-requirements.sh - displayName: Check GLIBC and GLIBCXX dependencies in remote/node_modules - - script: node build/azure-pipelines/distro/mixin-npm displayName: Mixin distro node modules @@ -172,9 +164,11 @@ steps: yarn gulp vscode-reh-linux-$(VSCODE_ARCH)-min-ci mv ../vscode-reh-linux-$(VSCODE_ARCH) ../vscode-server-linux-$(VSCODE_ARCH) # TODO@joaomoreno ARCHIVE_PATH=".build/linux/server/vscode-server-linux-legacy-$(VSCODE_ARCH).tar.gz" + UNARCHIVE_PATH="`pwd`/../vscode-server-linux-$(VSCODE_ARCH)" mkdir -p $(dirname $ARCHIVE_PATH) tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH) echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" + echo "##vso[task.setvariable variable=SERVER_UNARCHIVE_PATH]$UNARCHIVE_PATH" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server @@ -192,6 +186,26 @@ steps: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server (web) + - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: + - script: | + set -e + EXPECTED_GLIBC_VERSION="2.17" \ + EXPECTED_GLIBCXX_VERSION="3.4.19" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + env: + SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) + displayName: Check GLIBC and GLIBCXX dependencies in server archive + + - ${{ else }}: + - script: | + set -e + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.22" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + env: + SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) + displayName: Check GLIBC and GLIBCXX dependencies in server archive + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - template: product-build-linux-test.yml parameters: diff --git a/build/azure-pipelines/linux/product-build-linux-test.yml b/build/azure-pipelines/linux/product-build-linux-test.yml index 4968b9ff04f..f5c00aa0cf0 100644 --- a/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-test.yml @@ -100,9 +100,6 @@ steps: timeoutInMinutes: 20 - script: ./scripts/test-remote-integration.sh - env: - # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x - UV_USE_IO_URING: 0 displayName: Run integration tests (Remote) timeoutInMinutes: 20 @@ -137,8 +134,6 @@ steps: ./scripts/test-remote-integration.sh env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH) - # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x - UV_USE_IO_URING: 0 displayName: Run integration tests (Remote) timeoutInMinutes: 20 @@ -169,16 +164,10 @@ steps: - script: yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" timeoutInMinutes: 20 - env: - # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x - UV_USE_IO_URING: 0 displayName: Run smoke tests (Browser, Chromium) - script: yarn smoketest-no-compile --remote --tracing timeoutInMinutes: 20 - env: - # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x - UV_USE_IO_URING: 0 displayName: Run smoke tests (Remote) - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: @@ -189,8 +178,6 @@ steps: - script: yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)-web - # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x - UV_USE_IO_URING: 0 timeoutInMinutes: 20 displayName: Run smoke tests (Browser, Chromium) @@ -201,9 +188,6 @@ steps: VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)" \ yarn smoketest-no-compile --tracing --remote --build "$APP_PATH" timeoutInMinutes: 20 - env: - # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x - UV_USE_IO_URING: 0 displayName: Run smoke tests (Remote) - script: | diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 352b31360f8..d1d6bdb9191 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -131,16 +131,6 @@ steps: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: | - set -e - - EXPECTED_GLIBC_VERSION="2.28" \ - EXPECTED_GLIBCXX_VERSION="3.4.25" \ - ./build/azure-pipelines/linux/verify-glibc-requirements.sh - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Check GLIBC and GLIBCXX dependencies in remote/node_modules - - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: node build/azure-pipelines/distro/mixin-npm condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) @@ -213,9 +203,11 @@ steps: yarn gulp vscode-reh-linux-$(VSCODE_ARCH)-min-ci mv ../vscode-reh-linux-$(VSCODE_ARCH) ../vscode-server-linux-$(VSCODE_ARCH) # TODO@joaomoreno ARCHIVE_PATH=".build/linux/server/vscode-server-linux-$(VSCODE_ARCH).tar.gz" + UNARCHIVE_PATH="`pwd`/../vscode-server-linux-$(VSCODE_ARCH)" mkdir -p $(dirname $ARCHIVE_PATH) tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH) echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" + echo "##vso[task.setvariable variable=SERVER_UNARCHIVE_PATH]$UNARCHIVE_PATH" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server @@ -232,6 +224,36 @@ steps: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server (web) + - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: + - script: | + set -e + + source ./build/azure-pipelines/linux/setup-env.sh + + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.25" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + env: + SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) + npm_config_arch: $(NPM_ARCH) + VSCODE_ARCH: $(VSCODE_ARCH) + displayName: Check GLIBC and GLIBCXX dependencies in server archive + + - ${{ else }}: + - script: | + set -e + + source ./build/azure-pipelines/linux/setup-env.sh + + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.26" \ + ./build/azure-pipelines/linux/verify-glibc-requirements.sh + env: + SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) + npm_config_arch: $(NPM_ARCH) + VSCODE_ARCH: $(VSCODE_ARCH) + displayName: Check GLIBC and GLIBCXX dependencies in server archive + - ${{ else }}: - script: yarn gulp "transpile-client-swc" "transpile-extensions" env: diff --git a/build/azure-pipelines/linux/verify-glibc-requirements.sh b/build/azure-pipelines/linux/verify-glibc-requirements.sh index f07c0ba71b0..19482c242ea 100755 --- a/build/azure-pipelines/linux/verify-glibc-requirements.sh +++ b/build/azure-pipelines/linux/verify-glibc-requirements.sh @@ -9,8 +9,8 @@ elif [ "$VSCODE_ARCH" == "armhf" ]; then TRIPLE="arm-rpi-linux-gnueabihf" fi -# Get all files with .node extension from remote/node_modules folder -files=$(find remote/node_modules -name "*.node" -not -path "*prebuilds*") +# Get all files with .node extension from server folder +files=$(find $SEARCH_PATH -name "*.node" -not -path "*prebuilds*" -o -type f -executable -name "node") echo "Verifying requirements for files: $files" @@ -19,13 +19,13 @@ for file in $files; do glibcxx_version="$EXPECTED_GLIBCXX_VERSION" while IFS= read -r line; do if [[ $line == *"GLIBC_"* ]]; then - version=$(echo "$line" | awk '{print $5}' | tr -d '()') + version=$(echo "$line" | awk '{if ($5 ~ /^[0-9a-fA-F]+$/) print $6; else print $5}' | tr -d '()') version=${version#*_} if [[ $(printf "%s\n%s" "$version" "$glibc_version" | sort -V | tail -n1) == "$version" ]]; then glibc_version=$version fi elif [[ $line == *"GLIBCXX_"* ]]; then - version=$(echo "$line" | awk '{print $5}' | tr -d '()') + version=$(echo "$line" | awk '{if ($5 ~ /^[0-9a-fA-F]+$/) print $6; else print $5}' | tr -d '()') version=${version#*_} if [[ $(printf "%s\n%s" "$version" "$glibcxx_version" | sort -V | tail -n1) == "$version" ]]; then glibcxx_version=$version @@ -34,11 +34,11 @@ for file in $files; do done < <("$PWD/.build/sysroots/$TRIPLE/$TRIPLE/bin/objdump" -T "$file") if [[ "$glibc_version" != "$EXPECTED_GLIBC_VERSION" ]]; then - echo "Error: File $file has dependency on GLIBC > $EXPECTED_GLIBC_VERSION" + echo "Error: File $file has dependency on GLIBC > $EXPECTED_GLIBC_VERSION, found $glibc_version" exit 1 fi if [[ "$glibcxx_version" != "$EXPECTED_GLIBCXX_VERSION" ]]; then - echo "Error: File $file has dependency on GLIBCXX > $EXPECTED_GLIBCXX_VERSION" + echo "Error: File $file has dependency on GLIBCXX > $EXPECTED_GLIBCXX_VERSION, found $glibcxx_version" exit 1 fi done diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 0027a774b35..0c4f98aa511 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -164,10 +164,10 @@ resources: - repository: 1ESPipelines type: git name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/heads/joao/disable-tsa-linux-arm64 + ref: refs/tags/release extends: - template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + template: v1/1ES.Unofficial.PipelineTemplate.yml@1esPipelines parameters: sdl: tsa: @@ -183,7 +183,7 @@ extends: allTools: true codeql: compiled: - enabled: true + enabled: false runSourceLanguagesInSourceAnalysis: true credscan: suppressionsFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/CredScanSuppressions.json @@ -216,110 +216,124 @@ extends: parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - - stage: CompileCLI + - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - stage: CompileCLI + dependsOn: [] + jobs: + - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: + - job: CLILinuxX64 + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + steps: + - template: build/azure-pipelines/linux/cli-build-linux.yml@self + parameters: + VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_LINUX: ${{ parameters.VSCODE_BUILD_LINUX }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), or(eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true))) }}: + - job: CLILinuxGnuARM + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + steps: + - template: build/azure-pipelines/linux/cli-build-linux.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_LINUX_ARMHF: ${{ parameters.VSCODE_BUILD_LINUX_ARMHF }} + VSCODE_BUILD_LINUX_ARM64: ${{ parameters.VSCODE_BUILD_LINUX_ARM64 }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE, true)) }}: + - job: CLIAlpineX64 + pool: + name: 1es-ubuntu-20.04-x64 + os: linux + steps: + - template: build/azure-pipelines/alpine/cli-build-alpine.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_ALPINE: ${{ parameters.VSCODE_BUILD_ALPINE }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true)) }}: + - job: CLIAlpineARM64 + pool: + name: 1es-mariner-2.0-arm64 + os: linux + hostArchitecture: arm64 + container: ubuntu-2004-arm64 + steps: + - template: build/azure-pipelines/alpine/cli-build-alpine.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_ALPINE_ARM64: ${{ parameters.VSCODE_BUILD_ALPINE_ARM64 }} + + - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: + - job: CLIMacOSX64 + pool: + name: Azure Pipelines + image: macOS-13 + os: macOS + steps: + - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self + parameters: + VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_MACOS: ${{ parameters.VSCODE_BUILD_MACOS }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: + - job: CLIMacOSARM64 + pool: + name: Azure Pipelines + image: macOS-13 + os: macOS + steps: + - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_MACOS_ARM64: ${{ parameters.VSCODE_BUILD_MACOS_ARM64 }} + + - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: + - job: CLIWindowsX64 + pool: + name: 1es-windows-2019-x64 + os: windows + steps: + - template: build/azure-pipelines/win32/cli-build-win32.yml@self + parameters: + VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - job: CLIWindowsARM64 + pool: + name: 1es-windows-2019-x64 + os: windows + steps: + - template: build/azure-pipelines/win32/cli-build-win32.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} + + - stage: CustomSDL dependsOn: [] + pool: + name: 1es-windows-2019-x64 + os: windows jobs: - - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: - - job: CLILinuxX64 - pool: - name: 1es-ubuntu-20.04-x64 - os: linux - steps: - - template: build/azure-pipelines/linux/cli-build-linux.yml@self - parameters: - VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_LINUX: ${{ parameters.VSCODE_BUILD_LINUX }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), or(eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true))) }}: - - job: CLILinuxGnuARM - pool: - name: 1es-ubuntu-20.04-x64 - os: linux - steps: - - template: build/azure-pipelines/linux/cli-build-linux.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_LINUX_ARMHF: ${{ parameters.VSCODE_BUILD_LINUX_ARMHF }} - VSCODE_BUILD_LINUX_ARM64: ${{ parameters.VSCODE_BUILD_LINUX_ARM64 }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE, true)) }}: - - job: CLIAlpineX64 - pool: - name: 1es-ubuntu-20.04-x64 - os: linux - steps: - - template: build/azure-pipelines/alpine/cli-build-alpine.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_ALPINE: ${{ parameters.VSCODE_BUILD_ALPINE }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true)) }}: - - job: CLIAlpineARM64 - pool: - name: 1es-mariner-2.0-arm64 - os: linux - hostArchitecture: arm64 - container: ubuntu-2004-arm64 - steps: - - template: build/azure-pipelines/alpine/cli-build-alpine.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_ALPINE_ARM64: ${{ parameters.VSCODE_BUILD_ALPINE_ARM64 }} - - - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: - - job: CLIMacOSX64 - pool: - name: Azure Pipelines - image: macOS-11 - os: macOS - steps: - - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self - parameters: - VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_MACOS: ${{ parameters.VSCODE_BUILD_MACOS }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: - - job: CLIMacOSARM64 - pool: - name: Azure Pipelines - image: macOS-11 - os: macOS - steps: - - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_MACOS_ARM64: ${{ parameters.VSCODE_BUILD_MACOS_ARM64 }} - - - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - - job: CLIWindowsX64 - pool: - name: 1es-windows-2019-x64 - os: windows - steps: - - template: build/azure-pipelines/win32/cli-build-win32.yml@self - parameters: - VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: - - job: CLIWindowsARM64 - pool: - name: 1es-windows-2019-x64 - os: windows - steps: - - template: build/azure-pipelines/win32/cli-build-win32.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} + - job: WindowsSDL + variables: + - group: 'API Scan' + steps: + - template: build/azure-pipelines/sdl-scan.yml@self - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_WINDOWS'], true)) }}: - stage: Windows dependsOn: - Compile - - CompileCLI + - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - CompileCLI pool: name: 1es-windows-2019-x64 os: windows @@ -410,7 +424,8 @@ extends: - stage: Linux dependsOn: - Compile - - CompileCLI + - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - CompileCLI pool: name: 1es-ubuntu-20.04-x64 os: linux @@ -568,7 +583,8 @@ extends: - stage: Alpine dependsOn: - Compile - - CompileCLI + - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - CompileCLI pool: name: 1es-ubuntu-20.04-x64 os: linux @@ -594,10 +610,11 @@ extends: - stage: macOS dependsOn: - Compile - - CompileCLI + - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - CompileCLI pool: name: Azure Pipelines - image: macOS-11 + image: macOS-13 os: macOS variables: BUILDSECMON_OPT_IN: true diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 41c33f3f265..9a3748ed6fc 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -135,13 +135,22 @@ steps: - script: | set -e - AZURE_STORAGE_ACCOUNT="ticino" \ + AZURE_STORAGE_ACCOUNT="vscodeweb" \ AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ node build/azure-pipelines/upload-sourcemaps displayName: Upload sourcemaps to Azure + - script: | + set -e + AZURE_STORAGE_ACCOUNT="ticino" \ + AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ + AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ + AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ + node build/azure-pipelines/upload-sourcemaps + displayName: Upload sourcemaps to Azure (Deprecated) + - script: ./build/azure-pipelines/common/extract-telemetry.sh displayName: Generate lists of telemetry events diff --git a/build/azure-pipelines/sdl-scan.yml b/build/azure-pipelines/sdl-scan.yml index 927cd5e04ae..af20a305d9c 100644 --- a/build/azure-pipelines/sdl-scan.yml +++ b/build/azure-pipelines/sdl-scan.yml @@ -1,296 +1,151 @@ -trigger: none -pr: none - parameters: - name: NPM_REGISTRY displayName: "Custom NPM Registry" type: string default: "https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/npm/registry/" - - name: SCAN_WINDOWS - displayName: "Scan Windows" - type: boolean - default: true - - name: SCAN_LINUX - displayName: "Scan Linux" - type: boolean - default: false - -variables: - - name: NPM_REGISTRY - value: ${{ parameters.NPM_REGISTRY }} - - name: SCAN_WINDOWS - value: ${{ eq(parameters.SCAN_WINDOWS, true) }} - - name: SCAN_LINUX - value: ${{ eq(parameters.SCAN_LINUX, true) }} - - name: VSCODE_MIXIN_REPO - value: microsoft/vscode-distro - - name: skipComponentGovernanceDetection - value: true - name: NPM_ARCH - value: x64 + type: string + default: x64 - name: VSCODE_ARCH - value: x64 - - name: Codeql.enabled - value: true - - name: Codeql.TSAEnabled - value: true - - name: Codeql.TSAOptionsPath - value: '$(Build.SourcesDirectory)\build\azure-pipelines\config\tsaoptions.json' + type: string + default: x64 -stages: - - stage: Windows - condition: eq(variables.SCAN_WINDOWS, 'true') - pool: 1es-windows-2019-x64 - jobs: - - job: WindowsJob - timeoutInMinutes: 0 - steps: - - task: CredScan@3 - continueOnError: true - inputs: - scanFolder: "$(Build.SourcesDirectory)" - outputFormat: "pre" +steps: + - task: NodeTool@0 + inputs: + versionSource: fromFile + versionFilePath: .nvmrc + nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + - template: ./distro/download-distro.yml - - template: ./distro/download-distro.yml + - task: AzureKeyVault@1 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: "vscode-builds-subscription" + KeyVaultName: vscode-build-secrets + SecretsFilter: "github-distro-mixin-password" - - task: AzureKeyVault@1 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: "vscode-builds-subscription" - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm config set registry "${{ parameters.NPM_REGISTRY }}" --location=project } + # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb + # following is a workaround for yarn to send authorization header + # for GET requests to the registry. + exec { Add-Content -Path .npmrc -Value "always-auth=true" } + exec { yarn config set registry "${{ parameters.NPM_REGISTRY }}" } + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) + displayName: Setup NPM & Yarn - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npm config set registry "$env:NPM_REGISTRY" --location=project } - # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb - # following is a workaround for yarn to send authorization header - # for GET requests to the registry. - exec { Add-Content -Path .npmrc -Value "always-auth=true" } - exec { yarn config set registry "$env:NPM_REGISTRY" } - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM & Yarn + - task: npmAuthenticate@0 + inputs: + workingFile: .npmrc + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) + displayName: Setup NPM Authentication - - task: npmAuthenticate@0 - inputs: - workingFile: .npmrc - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { node build/setup-npm-registry.js "${{ parameters.NPM_REGISTRY }}" } + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.NPM_REGISTRY }}', 'none')) + displayName: Setup NPM Registry - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { node build/setup-npm-registry.js $env:NPM_REGISTRY } - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Registry + - pwsh: | + $includes = @' + { + 'target_defaults': { + 'conditions': [ + ['OS=="win"', { + 'msvs_configuration_attributes': { + 'SpectreMitigation': 'Spectre' + }, + 'msvs_settings': { + 'VCCLCompilerTool': { + 'AdditionalOptions': [ + '/Zi', + '/FS' + ], + }, + 'VCLinkerTool': { + 'AdditionalOptions': [ + '/profile' + ] + } + } + }] + ] + } + } + '@ - - task: CodeQL3000Init@0 - displayName: CodeQL Initialize - condition: eq(variables['Codeql.enabled'], 'True') + if (!(Test-Path "~/.gyp")) { + mkdir "~/.gyp" + } + echo $includes > "~/.gyp/include.gypi" + displayName: Create include.gypi - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - . build/azure-pipelines/win32/retry.ps1 - $ErrorActionPreference = "Stop" - # TODO: remove custom node-gyp when updating to Node v20, - # refs https://github.com/npm/cli/releases/tag/v10.2.3 which is available with Node >= 20.10.0 - $nodeGypDir = "$(Agent.TempDirectory)/custom-packages" - mkdir "$nodeGypDir" - npm install node-gyp@10.0.1 -g --prefix "$nodeGypDir" - $env:npm_config_node_gyp = "${nodeGypDir}/node_modules/node-gyp/bin/node-gyp.js" - $env:npm_config_arch = "$(NPM_ARCH)" - retry { exec { yarn --frozen-lockfile --check-files } } - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - CHILD_CONCURRENCY: 1 - displayName: Install dependencies + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + . build/azure-pipelines/win32/retry.ps1 + $ErrorActionPreference = "Stop" + retry { exec { yarn --frozen-lockfile --check-files } } + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + CHILD_CONCURRENCY: 1 + displayName: Install dependencies - - script: node build/azure-pipelines/distro/mixin-npm - displayName: Mixin distro node modules + - script: node build/azure-pipelines/distro/mixin-npm + displayName: Mixin distro node modules - - script: node build/azure-pipelines/distro/mixin-quality - displayName: Mixin distro quality - env: - VSCODE_QUALITY: stable + - script: node build/azure-pipelines/distro/mixin-quality + displayName: Mixin distro quality + env: + VSCODE_QUALITY: stable - - powershell: yarn compile - displayName: Compile + - powershell: yarn compile + displayName: Compile - - task: CodeQL3000Finalize@0 - displayName: CodeQL Finalize - condition: eq(variables['Codeql.enabled'], 'True') + - powershell: yarn gulp "vscode-symbols-win32-${{ parameters.VSCODE_ARCH }}" + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Download Symbols - - powershell: yarn gulp "vscode-symbols-win32-$(VSCODE_ARCH)" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Download Symbols + - task: BinSkim@4 + inputs: + InputType: "Basic" + Function: "analyze" + TargetPattern: "guardianGlob" + AnalyzeIgnorePdbLoadError: true + AnalyzeTargetGlob: '$(agent.builddirectory)\scanbin\**.dll;$(agent.builddirectory)\scanbin\**.exe;$(agent.builddirectory)\scanbin\**.node' + AnalyzeLocalSymbolDirectories: '$(agent.builddirectory)\scanbin\VSCode-win32-${{ parameters.VSCODE_ARCH }}\pdb' - - task: PSScriptAnalyzer@1 - inputs: - Path: '$(Build.SourcesDirectory)' - Settings: required - Recurse: true + - task: CopyFiles@2 + displayName: 'Collect Symbols for API Scan' + inputs: + SourceFolder: $(Agent.BuildDirectory) + Contents: 'scanbin\**\*.pdb' + TargetFolder: '$(agent.builddirectory)\symbols' + flattenFolders: true + condition: succeeded() - - task: BinSkim@4 - inputs: - InputType: "Basic" - Function: "analyze" - TargetPattern: "guardianGlob" - AnalyzeIgnorePdbLoadError: true - AnalyzeTargetGlob: '$(agent.builddirectory)\scanbin\**.dll;$(agent.builddirectory)\scanbin\**.exe;$(agent.builddirectory)\scanbin\**.node' - AnalyzeLocalSymbolDirectories: '$(agent.builddirectory)\scanbin\VSCode-win32-$(VSCODE_ARCH)\pdb' + # - task: APIScan@2 + # inputs: + # softwareFolder: $(agent.builddirectory)\scanbin + # softwareName: 'vscode-client' + # softwareVersionNum: '1' + # symbolsFolder: 'SRV*http://symweb;$(agent.builddirectory)\symbols' + # isLargeApp: false + # toolVersion: 'Latest' + # displayName: Run ApiScan + # condition: succeeded() + # env: + # AzureServicesAuthConnectionString: $(apiscan-connectionstring) - - task: AntiMalware@4 - inputs: - InputType: Basic - ScanType: CustomScan - FileDirPath: '$(Build.SourcesDirectory)' - EnableServices: true - SupportLogOnError: false - TreatSignatureUpdateFailureAs: 'Warning' - SignatureFreshness: 'OneDay' - TreatStaleSignatureAs: 'Error' - - - task: PublishSecurityAnalysisLogs@3 - inputs: - ArtifactName: CodeAnalysisLogs - ArtifactType: Container - PublishProcessedResults: false - AllTools: true - - - task: TSAUpload@2 - inputs: - GdnPublishTsaOnboard: true - GdnPublishTsaConfigFile: '$(Build.SourcesDirectory)\build\azure-pipelines\config\tsaoptions.json' - - - stage: Linux - dependsOn: [] - condition: eq(variables.SCAN_LINUX, 'true') - pool: - vmImage: "Ubuntu-18.04" - jobs: - - job: LinuxJob - steps: - - task: CredScan@2 - inputs: - toolMajorVersion: "V2" - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - - template: ./distro/download-distro.yml - - - task: AzureKeyVault@1 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: "vscode-builds-subscription" - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" - - - script: | - set -e - npm config set registry "$NPM_REGISTRY" --location=project - # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb - # following is a workaround for yarn to send authorization header - # for GET requests to the registry. - echo "always-auth=true" >> .npmrc - yarn config set registry "$NPM_REGISTRY" - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM & Yarn - - - task: npmAuthenticate@0 - inputs: - workingFile: .npmrc - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication - - - script: node build/setup-npm-registry.js $NPM_REGISTRY - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Registry - - - script: | - set -e - for i in {1..5}; do # try 5 times - yarn --cwd build --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - displayName: Install build dependencies - - - script: | - set -e - export npm_config_arch=$(NPM_ARCH) - - if [ -z "$CC" ] || [ -z "$CXX" ]; then - # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/96.0.4664.110/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux - # Download libcxx headers and objects from upstream electron releases - DEBUG=libcxx-fetcher \ - VSCODE_LIBCXX_OBJECTS_DIR=$PWD/.build/libcxx-objects \ - VSCODE_LIBCXX_HEADERS_DIR=$PWD/.build/libcxx_headers \ - VSCODE_LIBCXXABI_HEADERS_DIR=$PWD/.build/libcxxabi_headers \ - VSCODE_ARCH="$(NPM_ARCH)" \ - node build/linux/libcxx-fetcher.js - # Set compiler toolchain - export CC=$PWD/.build/CR_Clang/bin/clang - export CXX=$PWD/.build/CR_Clang/bin/clang++ - export CXXFLAGS="-std=c++17 -nostdinc++ -D__NO_INLINE__ -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr" - export LDFLAGS="-stdlib=libc++ -fuse-ld=lld -flto=thin -fsplit-lto-unit -L$PWD/.build/libcxx-objects -lc++abi" - export VSCODE_REMOTE_CC=$(which gcc) - export VSCODE_REMOTE_CXX=$(which g++) - fi - - for i in {1..5}; do # try 5 times - yarn --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Install dependencies - - - script: yarn --frozen-lockfile --check-files - workingDirectory: .build/distro/npm - env: - npm_config_arch: $(NPM_ARCH) - displayName: Install distro node modules - - - script: node build/azure-pipelines/distro/mixin-npm - displayName: Mixin distro node modules - - - script: node build/azure-pipelines/distro/mixin-quality - displayName: Mixin distro quality - env: - VSCODE_QUALITY: stable - - - script: yarn gulp vscode-symbols-linux-$(VSCODE_ARCH) - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build - - - task: BinSkim@3 - inputs: - toolVersion: Latest - InputType: CommandLine - arguments: analyze $(agent.builddirectory)\scanbin\exe\*.* --recurse --local-symbol-directories $(agent.builddirectory)\scanbin\VSCode-linux-$(VSCODE_ARCH)\pdb - - - task: TSAUpload@2 - inputs: - GdnPublishTsaConfigFile: '$(Build.SourceDirectory)\build\azure-pipelines\config\tsaoptions.json' + - task: PublishSecurityAnalysisLogs@3 + inputs: + ArtifactName: CodeAnalysisLogs + ArtifactType: Container + PublishProcessedResults: false + AllTools: true diff --git a/build/azure-pipelines/upload-nlsmetadata.js b/build/azure-pipelines/upload-nlsmetadata.js index 34c2005a30f..5b6cd3ed1fd 100644 --- a/build/azure-pipelines/upload-nlsmetadata.js +++ b/build/azure-pipelines/upload-nlsmetadata.js @@ -16,13 +16,33 @@ const commit = process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientSecretCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_CLIENT_SECRET']); function main() { return new Promise((c, e) => { - es.merge(vfs.src('out-vscode-web-min/nls.metadata.json', { base: 'out-vscode-web-min' }), vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })) + const combinedMetadataJson = es.merge( + // vscode: we are not using `out-build/nls.metadata.json` here because + // it includes metadata for translators for `keys`. but for our purpose + // we want only the `keys` and `messages` as `string`. + es.merge(vfs.src('out-build/nls.keys.json', { base: 'out-build' }), vfs.src('out-build/nls.messages.json', { base: 'out-build' })) .pipe(merge({ + fileName: 'vscode.json', + jsonSpace: '', + concatArrays: true, + edit: (parsedJson, file) => { + if (file.base === 'out-build') { + if (file.basename === 'nls.keys.json') { + return { keys: parsedJson }; + } + else { + return { messages: parsedJson }; + } + } + } + })), + // extensions + vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })).pipe(merge({ fileName: 'combined.nls.metadata.json', jsonSpace: '', concatArrays: true, edit: (parsedJson, file) => { - if (file.base === 'out-vscode-web-min') { + if (file.basename === 'vscode.json') { return { vscode: parsedJson }; } // Handle extensions and follow the same structure as the Core nls file. @@ -72,13 +92,15 @@ function main() { const key = manifestJson.publisher + '.' + manifestJson.name; return { [key]: parsedJson }; }, - })) + })); + const nlsMessagesJs = vfs.src('out-build/nls.messages.js', { base: 'out-build' }); + es.merge(combinedMetadataJson, nlsMessagesJs) .pipe(gzip({ append: false })) .pipe(vfs.dest('./nlsMetadata')) .pipe(es.through(function (data) { console.log(`Uploading ${data.path}`); // trigger artifact upload - console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=combined.nls.metadata.json]${data.path}`); + console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=${data.basename}]${data.path}`); this.emit('data', data); })) .pipe(azure.upload({ diff --git a/build/azure-pipelines/upload-nlsmetadata.ts b/build/azure-pipelines/upload-nlsmetadata.ts index 416d0eec408..030cc8f0e5a 100644 --- a/build/azure-pipelines/upload-nlsmetadata.ts +++ b/build/azure-pipelines/upload-nlsmetadata.ts @@ -24,79 +24,103 @@ interface NlsMetadata { function main(): Promise { return new Promise((c, e) => { - - es.merge( - vfs.src('out-vscode-web-min/nls.metadata.json', { base: 'out-vscode-web-min' }), - vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), - vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), - vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })) - .pipe(merge({ - fileName: 'combined.nls.metadata.json', - jsonSpace: '', - concatArrays: true, - edit: (parsedJson, file) => { - if (file.base === 'out-vscode-web-min') { - return { vscode: parsedJson }; - } - - // Handle extensions and follow the same structure as the Core nls file. - switch (file.basename) { - case 'package.nls.json': - // put package.nls.json content in Core NlsMetadata format - // language packs use the key "package" to specify that - // translations are for the package.json file - parsedJson = { - messages: { - package: Object.values(parsedJson) - }, - keys: { - package: Object.keys(parsedJson) - }, - bundles: { - main: ['package'] - } - }; - break; - - case 'nls.metadata.header.json': - parsedJson = { header: parsedJson }; - break; - - case 'nls.metadata.json': { - // put nls.metadata.json content in Core NlsMetadata format - const modules = Object.keys(parsedJson); - - const json: NlsMetadata = { - keys: {}, - messages: {}, - bundles: { - main: [] - } - }; - for (const module of modules) { - json.messages[module] = parsedJson[module].messages; - json.keys[module] = parsedJson[module].keys; - json.bundles.main.push(module); + const combinedMetadataJson = es.merge( + // vscode: we are not using `out-build/nls.metadata.json` here because + // it includes metadata for translators for `keys`. but for our purpose + // we want only the `keys` and `messages` as `string`. + es.merge( + vfs.src('out-build/nls.keys.json', { base: 'out-build' }), + vfs.src('out-build/nls.messages.json', { base: 'out-build' })) + .pipe(merge({ + fileName: 'vscode.json', + jsonSpace: '', + concatArrays: true, + edit: (parsedJson, file) => { + if (file.base === 'out-build') { + if (file.basename === 'nls.keys.json') { + return { keys: parsedJson }; + } else { + return { messages: parsedJson }; } - parsedJson = json; - break; } } + })), - // Get extension id and use that as the key - const folderPath = path.join(file.base, file.relative.split('/')[0]); - const manifest = readFileSync(path.join(folderPath, 'package.json'), 'utf-8'); - const manifestJson = JSON.parse(manifest); - const key = manifestJson.publisher + '.' + manifestJson.name; - return { [key]: parsedJson }; - }, - })) + // extensions + vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), + vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), + vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' }) + ).pipe(merge({ + fileName: 'combined.nls.metadata.json', + jsonSpace: '', + concatArrays: true, + edit: (parsedJson, file) => { + if (file.basename === 'vscode.json') { + return { vscode: parsedJson }; + } + + // Handle extensions and follow the same structure as the Core nls file. + switch (file.basename) { + case 'package.nls.json': + // put package.nls.json content in Core NlsMetadata format + // language packs use the key "package" to specify that + // translations are for the package.json file + parsedJson = { + messages: { + package: Object.values(parsedJson) + }, + keys: { + package: Object.keys(parsedJson) + }, + bundles: { + main: ['package'] + } + }; + break; + + case 'nls.metadata.header.json': + parsedJson = { header: parsedJson }; + break; + + case 'nls.metadata.json': { + // put nls.metadata.json content in Core NlsMetadata format + const modules = Object.keys(parsedJson); + + const json: NlsMetadata = { + keys: {}, + messages: {}, + bundles: { + main: [] + } + }; + for (const module of modules) { + json.messages[module] = parsedJson[module].messages; + json.keys[module] = parsedJson[module].keys; + json.bundles.main.push(module); + } + parsedJson = json; + break; + } + } + + // Get extension id and use that as the key + const folderPath = path.join(file.base, file.relative.split('/')[0]); + const manifest = readFileSync(path.join(folderPath, 'package.json'), 'utf-8'); + const manifestJson = JSON.parse(manifest); + const key = manifestJson.publisher + '.' + manifestJson.name; + return { [key]: parsedJson }; + }, + })); + + const nlsMessagesJs = vfs.src('out-build/nls.messages.js', { base: 'out-build' }); + + es.merge(combinedMetadataJson, nlsMessagesJs) .pipe(gzip({ append: false })) .pipe(vfs.dest('./nlsMetadata')) .pipe(es.through(function (data: Vinyl) { console.log(`Uploading ${data.path}`); // trigger artifact upload - console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=combined.nls.metadata.json]${data.path}`); + console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=${data.basename}]${data.path}`); this.emit('data', data); })) .pipe(azure.upload({ diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index bf43d9212cf..a522e845f3b 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -129,6 +129,15 @@ steps: node build/azure-pipelines/upload-cdn displayName: Upload to CDN + - script: | + set -e + AZURE_STORAGE_ACCOUNT="vscodeweb" \ + AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ + AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ + AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ + node build/azure-pipelines/upload-sourcemaps out-vscode-web-min out-vscode-web-min/vs/workbench/workbench.web.main.js.map + displayName: Upload sourcemaps (Web) + # upload only the workbench.web.main.js source maps because # we just compiled these bits in the previous step and the # general task to upload source maps has already been run @@ -139,11 +148,11 @@ steps: AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ node build/azure-pipelines/upload-sourcemaps out-vscode-web-min out-vscode-web-min/vs/workbench/workbench.web.main.js.map - displayName: Upload sourcemaps (Web) + displayName: Upload sourcemaps (Deprecated) - script: | set -e - AZURE_STORAGE_ACCOUNT="ticino" \ + AZURE_STORAGE_ACCOUNT="vscodeweb" \ AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_CLIENT_SECRET="$(AZURE_CLIENT_SECRET)" \ diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index a3b251b71ac..ce791c094e6 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -72,6 +72,11 @@ steps: } displayName: Build integration tests + - powershell: .\build\azure-pipelines\win32\listprocesses.bat + displayName: Diagnostics before integration test runs + continueOnError: true + condition: succeededOrFailed() + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - powershell: .\scripts\test-integration.bat --tfs "Integration Tests" displayName: Run integration tests (Electron) @@ -121,6 +126,11 @@ steps: displayName: Run integration tests (Remote) timeoutInMinutes: 20 + - powershell: .\build\azure-pipelines\win32\listprocesses.bat + displayName: Diagnostics after integration test runs + continueOnError: true + condition: succeededOrFailed() + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - powershell: .\build\azure-pipelines\win32\listprocesses.bat displayName: Diagnostics before smoke test run diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index 63d24a93ab6..bcc9340406d 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,7 +1,7 @@ -31d2d46ae8d8a3982f54e2ff1e60c2e4a8e80bf78a3e8b46dcaac95ac5d7ce6a node-v20.9.0-darwin-arm64.tar.gz -fc5b73f2a78c17bbe926cdb1447d652f9f094c79582f1be6471b4b38a2e1ccc8 node-v20.9.0-darwin-x64.tar.gz -d2a7dbeeb274bfd16b579d2cafb92f673010df36c83a5b55de3916aad6806a6a node-v20.9.0-linux-arm64.tar.gz -a28a0de05177106d241ef426b3e006022bc7d242224adace7565868bd9ee6c06 node-v20.9.0-linux-armv7l.tar.gz -f0919f092fbf74544438907fa083c21e76b2d7a4bc287f0607ada1553ef16f60 node-v20.9.0-linux-x64.tar.gz -54e165b89e75158993910053db5b0e652c1826521e624126de5ca6de9ff7b06d win-arm64/node.exe -538140015da83597ea7e7ef5e108ebac8a2dc4784b2a4134222b6c27c39f90ad win-x64/node.exe +e0065c61f340e85106a99c4b54746c5cee09d59b08c5712f67f99e92aa44995d node-v20.11.1-darwin-arm64.tar.gz +c52e7fb0709dbe63a4cbe08ac8af3479188692937a7bd8e776e0eedfa33bb848 node-v20.11.1-darwin-x64.tar.gz +e34ab2fc2726b4abd896bcbff0250e9b2da737cbd9d24267518a802ed0606f3b node-v20.11.1-linux-arm64.tar.gz +e42791f76ece283c7a4b97fbf716da72c5128c54a9779f10f03ae74a4bcfb8f6 node-v20.11.1-linux-armv7l.tar.gz +bf3a779bef19452da90fb88358ec2c57e0d2f882839b20dc6afc297b6aafc0d7 node-v20.11.1-linux-x64.tar.gz +a5a9d30a8f7d56e00ccb27c1a7d24c8d0bc96a2689ebba8eb7527698793496f1 win-arm64/node.exe +bc585910690318aaebe3c57669cb83ca9d1e5791efd63195e238f54686e6c2ec win-x64/node.exe diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index 7da8e55c908..85d27273861 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -28,7 +28,6 @@ async function main(buildDir) { x64AsarPath, arm64AsarPath, filesToSkip: [ - 'product.json', 'Credits.rtf', 'CodeResources', 'fsevents.node', diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index ffba8952cd8..04eb3a11e20 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -32,7 +32,6 @@ async function main(buildDir?: string) { x64AsarPath, arm64AsarPath, filesToSkip: [ - 'product.json', 'Credits.rtf', 'CodeResources', 'fsevents.node', diff --git a/build/filters.js b/build/filters.js index c7be2d818d9..915240f0f0b 100644 --- a/build/filters.js +++ b/build/filters.js @@ -199,7 +199,7 @@ module.exports.eslintFilter = [ .toString().split(/\r\n|\n/) .filter(line => !line.startsWith('#')) .filter(line => !!line) - .map(line => `!${line}`) + .map(line => line.startsWith('!') ? line.slice(1) : `!${line}`) ]; module.exports.stylelintFilter = [ diff --git a/build/gulpfile.cli.js b/build/gulpfile.cli.js index 86646fdb274..592fc74516c 100644 --- a/build/gulpfile.cli.js +++ b/build/gulpfile.cli.js @@ -5,8 +5,6 @@ 'use strict'; -//@ts-check - const es = require('event-stream'); const gulp = require('gulp'); const path = require('path'); @@ -24,7 +22,6 @@ const createReporter = require('./lib/reporter').createReporter; const root = 'cli'; const rootAbs = path.resolve(__dirname, '..', root); const src = `${root}/src`; -const targetCliPath = path.join(root, 'target', 'debug', process.platform === 'win32' ? 'code.exe' : 'code'); const platformOpensslDirName = process.platform === 'win32' ? ( diff --git a/build/gulpfile.compile.js b/build/gulpfile.compile.js index c4947e76cbf..de8f3c4fb57 100644 --- a/build/gulpfile.compile.js +++ b/build/gulpfile.compile.js @@ -3,18 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +//@ts-check 'use strict'; const gulp = require('gulp'); const util = require('./lib/util'); +const date = require('./lib/date'); const task = require('./lib/task'); const compilation = require('./lib/compilation'); const optimize = require('./lib/optimize'); +/** + * @param {boolean} disableMangle + */ function makeCompileBuildTask(disableMangle) { return task.series( util.rimraf('out-build'), util.buildWebNodePaths('out-build'), + date.writeISODate('out-build'), compilation.compileApiProposalNamesTask, compilation.compileTask('src', 'out-build', true, { disableMangle }), optimize.optimizeLoaderTask('out-build', 'out-build', true) diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 22b70a953df..fe29d4fe183 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -29,19 +29,17 @@ const editorEntryPoints = [ { name: 'vs/editor/editor.main', include: [], - exclude: ['vs/css', 'vs/nls'], + exclude: ['vs/css'], prepend: [ - { path: 'out-editor-build/vs/css.js', amdModuleId: 'vs/css' }, - { path: 'out-editor-build/vs/nls.js', amdModuleId: 'vs/nls' } + { path: 'out-editor-build/vs/css.js', amdModuleId: 'vs/css' } ], }, { name: 'vs/base/common/worker/simpleWorker', include: ['vs/editor/common/services/editorSimpleWorker'], - exclude: ['vs/nls'], + exclude: [], prepend: [ { path: 'vs/loader.js' }, - { path: 'vs/nls.js', amdModuleId: 'vs/nls' }, { path: 'vs/base/worker/workerMain.js' } ], dest: 'vs/base/worker/workerMain.js' @@ -86,7 +84,8 @@ const extractEditorSrcTask = task.define('extract-editor-src', () => { }); // Disable mangling for the editor, as it complicates debugging & quite a few users rely on private/protected fields. -const compileEditorAMDTask = task.define('compile-editor-amd', compilation.compileTask('out-editor-src', 'out-editor-build', true, { disableMangle: true })); +// Disable NLS task to remove english strings to preserve backwards compatibility when we removed the `vs/nls!` AMD plugin. +const compileEditorAMDTask = task.define('compile-editor-amd', compilation.compileTask('out-editor-src', 'out-editor-build', true, { disableMangle: true, preserveEnglish: true })); const optimizeEditorAMDTask = task.define('optimize-editor-amd', optimize.optimizeTask( { @@ -99,7 +98,6 @@ const optimizeEditorAMDTask = task.define('optimize-editor-amd', optimize.optimi paths: { 'vs': 'out-editor-build/vs', 'vs/css': 'out-editor-build/vs/css.build', - 'vs/nls': 'out-editor-build/vs/nls.build', 'vscode': 'empty:' } }, @@ -124,7 +122,6 @@ const createESMSourcesAndResourcesTask = task.define('extract-editor-esm', () => 'vs/base/worker/workerMain.ts', ], renames: { - 'vs/nls.mock.ts': 'vs/nls.ts' } }); }); diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 55904959778..b85425bccfc 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -71,7 +71,7 @@ const compilations = [ '.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json', ]; -const getBaseUrl = out => `https://ticino.blob.core.windows.net/sourcemaps/${commit}/${out}`; +const getBaseUrl = out => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`; const tasks = compilations.map(function (tsconfigFile) { const absolutePath = path.join(root, tsconfigFile); diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index c2b81d0cf7c..d7a814b9a1b 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -12,11 +12,13 @@ const util = require('./lib/util'); const { getVersion } = require('./lib/getVersion'); const task = require('./lib/task'); const optimize = require('./lib/optimize'); +const { inlineMeta } = require('./lib/inlineMeta'); const product = require('../product.json'); const rename = require('gulp-rename'); const replace = require('gulp-replace'); const filter = require('gulp-filter'); const { getProductionDependencies } = require('./lib/dependencies'); +const { readISODate } = require('./lib/date'); const vfs = require('vinyl-fs'); const packageJson = require('../package.json'); const flatmap = require('gulp-flatmap'); @@ -52,14 +54,8 @@ const BUILD_TARGETS = [ const serverResources = [ - // Bootstrap - 'out-build/bootstrap.js', - 'out-build/bootstrap-fork.js', - 'out-build/bootstrap-amd.js', - 'out-build/bootstrap-node.js', - - // Performance - 'out-build/vs/base/common/performance.js', + // NLS + 'out-build/nls.messages.json', // Process monitor 'out-build/vs/base/node/cpuUsage.sh', @@ -89,23 +85,23 @@ const serverWithWebResources = [ const serverEntryPoints = [ { name: 'vs/server/node/server.main', - exclude: ['vs/css', 'vs/nls'] + exclude: ['vs/css'] }, { name: 'vs/server/node/server.cli', - exclude: ['vs/css', 'vs/nls'] + exclude: ['vs/css'] }, { name: 'vs/workbench/api/node/extensionHostProcess', - exclude: ['vs/css', 'vs/nls'] + exclude: ['vs/css'] }, { name: 'vs/platform/files/node/watcher/watcherMain', - exclude: ['vs/css', 'vs/nls'] + exclude: ['vs/css'] }, { name: 'vs/platform/terminal/node/ptyHostMain', - exclude: ['vs/css', 'vs/nls'] + exclude: ['vs/css'] } ]; @@ -118,6 +114,12 @@ const serverWithWebEntryPoints = [ ...vscodeWebEntryPoints ]; +const commonJSEntryPoints = [ + 'out-build/server-main.js', + 'out-build/server-cli.js', + 'out-build/bootstrap-fork.js', +]; + function getNodeVersion() { const yarnrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.yarnrc'), 'utf8'); const nodeVersion = /^target "(.*)"$/m.exec(yarnrc)[1]; @@ -129,7 +131,8 @@ function getNodeChecksum(nodeVersion, platform, arch, glibcPrefix) { let expectedName; switch (platform) { case 'win32': - expectedName = `win-${arch}/node.exe`; + expectedName = product.nodejsRepository !== 'https://nodejs.org' ? + `win-${arch}-node.exe` : `win-${arch}/node.exe`; break; case 'darwin': @@ -182,7 +185,6 @@ if (defaultNodeTask) { function nodejs(platform, arch) { const { fetchUrls, fetchGithub } = require('./lib/fetch'); const untar = require('gulp-untar'); - const crypto = require('crypto'); if (arch === 'armhf') { arch = 'armv7l'; @@ -288,13 +290,22 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa } const name = product.nameShort; + + let packageJsonContents; const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' }) - .pipe(json({ name, version, dependencies: undefined, optionalDependencies: undefined })); - - const date = new Date().toISOString(); + .pipe(json({ name, version, dependencies: undefined, optionalDependencies: undefined })) + .pipe(es.through(function (file) { + packageJsonContents = file.contents.toString(); + this.emit('data', file); + })); + let productJsonContents; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(json({ commit, date, version })); + .pipe(json({ commit, date: readISODate('out-build'), version })) + .pipe(es.through(function (file) { + productJsonContents = file.contents.toString(); + this.emit('data', file); + })); const license = gulp.src(['remote/LICENSE'], { base: 'remote', allowEmpty: true }); @@ -387,6 +398,12 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa ); } + result = inlineMeta(result, { + targetPaths: commonJSEntryPoints, + packageJsonFn: () => packageJsonContents, + productJsonFn: () => productJsonContents + }); + return result.pipe(vfs.dest(destination)); }; } @@ -418,16 +435,14 @@ function tweakProductForServerWeb(product) { }, commonJS: { src: 'out-build', - entryPoints: [ - 'out-build/server-main.js', - 'out-build/server-cli.js' - ], + entryPoints: commonJSEntryPoints, platform: 'node', external: [ 'minimist', - // TODO: we cannot inline `product.json` because + // We cannot inline `product.json` from here because // it is being changed during build time at a later // point in time (such as `checksums`) + // We have a manual step to inline these later. '../product.json', '../package.json' ] @@ -439,7 +454,7 @@ function tweakProductForServerWeb(product) { const minifyTask = task.define(`minify-vscode-${type}`, task.series( optimizeTask, util.rimraf(`out-vscode-${type}-min`), - optimize.minifyTask(`out-vscode-${type}`, `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) + optimize.minifyTask(`out-vscode-${type}`, `https://main.vscode-cdn.net/sourcemaps/${commit}/core`) )); gulp.task(minifyTask); diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index e1507e0424f..b3b35466af0 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -6,10 +6,7 @@ 'use strict'; const gulp = require('gulp'); -const merge = require('gulp-merge-json'); const fs = require('fs'); -const os = require('os'); -const cp = require('child_process'); const path = require('path'); const es = require('event-stream'); const vfs = require('vinyl-fs'); @@ -18,9 +15,11 @@ const replace = require('gulp-replace'); const filter = require('gulp-filter'); const util = require('./lib/util'); const { getVersion } = require('./lib/getVersion'); +const { readISODate } = require('./lib/date'); const task = require('./lib/task'); const buildfile = require('../src/buildfile'); const optimize = require('./lib/optimize'); +const { inlineMeta } = require('./lib/inlineMeta'); const root = path.dirname(__dirname); const commit = getVersion(root); const packageJson = require('../package.json'); @@ -51,16 +50,12 @@ const vscodeEntryPoints = [ ].flat(); const vscodeResources = [ - 'out-build/bootstrap.js', - 'out-build/bootstrap-fork.js', - 'out-build/bootstrap-amd.js', - 'out-build/bootstrap-node.js', - 'out-build/bootstrap-window.js', + 'out-build/nls.messages.json', + 'out-build/nls.keys.json', 'out-build/vs/**/*.{svg,png,html,jpg,mp3}', '!out-build/vs/code/browser/**/*.html', '!out-build/vs/code/**/*-dev.html', '!out-build/vs/editor/standalone/**/*.svg', - 'out-build/vs/base/common/performance.js', 'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh,cpuUsage.sh,ps.sh}', 'out-build/vs/base/browser/ui/codicons/codicon/**', 'out-build/vs/base/parts/sandbox/electron-sandbox/preload.js', @@ -73,6 +68,8 @@ const vscodeResources = [ 'out-build/vs/workbench/contrib/terminal/browser/media/*.sh', 'out-build/vs/workbench/contrib/terminal/browser/media/*.zsh', 'out-build/vs/workbench/contrib/webview/browser/pre/*.js', + '!out-build/vs/workbench/contrib/issue/browser/*.html', + '!out-build/vs/workbench/contrib/issue/**/*-dev.html', 'out-build/vs/**/markdown.css', 'out-build/vs/workbench/contrib/tasks/**/*.json', '!**/test/**' @@ -87,6 +84,12 @@ const windowBootstrapFiles = [ 'out-build/bootstrap-window.js' ]; +const commonJSEntryPoints = [ + 'out-build/main.js', + 'out-build/cli.js', + 'out-build/bootstrap-fork.js' +]; + const optimizeVSCodeTask = task.define('optimize-vscode', task.series( util.rimraf('out-vscode'), // Optimize: bundles source files automatically based on @@ -105,24 +108,24 @@ const optimizeVSCodeTask = task.define('optimize-vscode', task.series( }, commonJS: { src: 'out-build', - entryPoints: [ - 'out-build/main.js', - 'out-build/cli.js' - ], + entryPoints: commonJSEntryPoints, platform: 'node', external: [ 'electron', 'minimist', - // TODO: we cannot inline `product.json` because + 'original-fs', + // We cannot inline `product.json` from here because // it is being changed during build time at a later // point in time (such as `checksums`) + // We have a manual step to inline these later. '../product.json', '../package.json', ] }, manual: [ { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/workbench/workbench.js'], out: 'vs/code/electron-sandbox/workbench/workbench.js' }, - { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/issue/issueReporter.js'], out: 'vs/code/electron-sandbox/issue/issueReporter.js' }, + // TODO: @justchen https://github.com/microsoft/vscode/issues/213332 make sure to remove when we use window.open on desktop. + { src: [...windowBootstrapFiles, 'out-build/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js'], out: 'vs/workbench/contrib/issue/electron-sandbox/issueReporter.js' }, { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/processExplorer/processExplorer.js'], out: 'vs/code/electron-sandbox/processExplorer/processExplorer.js' } ] } @@ -130,7 +133,7 @@ const optimizeVSCodeTask = task.define('optimize-vscode', task.series( )); gulp.task(optimizeVSCodeTask); -const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; const minifyVSCodeTask = task.define('minify-vscode', task.series( optimizeVSCodeTask, util.rimraf('out-vscode-min'), @@ -246,14 +249,21 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op packageJsonUpdates.desktopName = `${product.applicationName}-url-handler.desktop`; } + let packageJsonContents; const packageJsonStream = gulp.src(['package.json'], { base: '.' }) - .pipe(json(packageJsonUpdates)); - - const date = new Date().toISOString(); - const productJsonUpdate = { commit, date, checksums, version }; + .pipe(json(packageJsonUpdates)) + .pipe(es.through(function (file) { + packageJsonContents = file.contents.toString(); + this.emit('data', file); + })); + let productJsonContents; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(json(productJsonUpdate)); + .pipe(json({ commit, date: readISODate('out-build'), checksums, version })) + .pipe(es.through(function (file) { + productJsonContents = file.contents.toString(); + this.emit('data', file); + })); const license = gulp.src([product.licenseFileName, 'ThirdPartyNotices.txt', 'licenses/**'], { base: '.', allowEmpty: true }); @@ -386,6 +396,12 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op .pipe(rename('bin/' + product.applicationName))); } + result = inlineMeta(result, { + targetPaths: commonJSEntryPoints, + packageJsonFn: () => packageJsonContents, + productJsonFn: () => productJsonContents + }); + return result.pipe(vfs.dest(destination)); }; } @@ -493,17 +509,12 @@ gulp.task(task.define( core, compileExtensionsBuildTask, function () { - const pathToMetadata = './out-vscode/nls.metadata.json'; - const pathToRehWebMetadata = './out-vscode-reh-web/nls.metadata.json'; + const pathToMetadata = './out-build/nls.metadata.json'; const pathToExtensions = '.build/extensions/*'; const pathToSetup = 'build/win32/i18n/messages.en.isl'; return es.merge( - gulp.src([pathToMetadata, pathToRehWebMetadata]).pipe(merge({ - fileName: 'nls.metadata.json', - jsonSpace: '', - concatArrays: true - })).pipe(i18n.createXlfFilesForCoreBundle()), + gulp.src(pathToMetadata).pipe(i18n.createXlfFilesForCoreBundle()), gulp.src(pathToSetup).pipe(i18n.createXlfFilesForIsl()), gulp.src(pathToExtensions).pipe(i18n.createXlfFilesForExtensions()) ).pipe(vfs.dest('../vscode-translations-export')); diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 8c2b62f7b2a..28ddfb04c3d 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -8,10 +8,9 @@ const gulp = require('gulp'); const replace = require('gulp-replace'); const rename = require('gulp-rename'); -const shell = require('gulp-shell'); const es = require('event-stream'); const vfs = require('vinyl-fs'); -const util = require('./lib/util'); +const { rimraf } = require('./lib/util'); const { getVersion } = require('./lib/getVersion'); const task = require('./lib/task'); const packageJson = require('../package.json'); @@ -19,6 +18,10 @@ const product = require('../product.json'); const dependenciesGenerator = require('./linux/dependencies-generator'); const debianRecommendedDependencies = require('./linux/debian/dep-lists').recommendedDeps; const path = require('path'); +const cp = require('child_process'); +const util = require('util'); + +const exec = util.promisify(cp.exec); const root = path.dirname(__dirname); const commit = getVersion(root); @@ -116,11 +119,13 @@ function prepareDebPackage(arch) { */ function buildDebPackage(arch) { const debArch = getDebPackageArch(arch); - return shell.task([ - 'chmod 755 ' + product.applicationName + '-' + debArch + '/DEBIAN/postinst ' + product.applicationName + '-' + debArch + '/DEBIAN/prerm ' + product.applicationName + '-' + debArch + '/DEBIAN/postrm', - 'mkdir -p deb', - 'fakeroot dpkg-deb -b ' + product.applicationName + '-' + debArch + ' deb' - ], { cwd: '.build/linux/deb/' + debArch }); + const cwd = `.build/linux/deb/${debArch}`; + + return async () => { + await exec(`chmod 755 ${product.applicationName}-${debArch}/DEBIAN/postinst ${product.applicationName}-${debArch}/DEBIAN/prerm ${product.applicationName}-${debArch}/DEBIAN/postrm`, { cwd }); + await exec('mkdir -p deb', { cwd }); + await exec(`fakeroot dpkg-deb -b ${product.applicationName}-${debArch} deb`, { cwd }); + }; } /** @@ -218,14 +223,14 @@ function prepareRpmPackage(arch) { function buildRpmPackage(arch) { const rpmArch = getRpmPackageArch(arch); const rpmBuildPath = getRpmBuildPath(rpmArch); - const rpmOut = rpmBuildPath + '/RPMS/' + rpmArch; - const destination = '.build/linux/rpm/' + rpmArch; + const rpmOut = `${rpmBuildPath}/RPMS/${rpmArch}`; + const destination = `.build/linux/rpm/${rpmArch}`; - return shell.task([ - 'mkdir -p ' + destination, - 'HOME="$(pwd)/' + destination + '" rpmbuild -bb ' + rpmBuildPath + '/SPECS/' + product.applicationName + '.spec --target=' + rpmArch, - 'cp "' + rpmOut + '/$(ls ' + rpmOut + ')" ' + destination + '/' - ]); + return async () => { + await exec(`mkdir -p ${destination}`); + await exec(`HOME="$(pwd)/${destination}" rpmbuild -bb ${rpmBuildPath}/SPECS/${product.applicationName}.spec --target=${rpmArch}`); + await exec(`cp "${rpmOut}/$(ls ${rpmOut})" ${destination}/`); + }; } /** @@ -286,9 +291,8 @@ function prepareSnapPackage(arch) { * @param {string} arch */ function buildSnapPackage(arch) { - const snapBuildPath = getSnapBuildPath(arch); - // Default target for snapcraft runs: pull, build, stage and prime, and finally assembles the snap. - return shell.task(`cd ${snapBuildPath} && snapcraft`); + const cwd = getSnapBuildPath(arch); + return () => exec('snapcraft', { cwd }); } const BUILD_TARGETS = [ @@ -299,18 +303,18 @@ const BUILD_TARGETS = [ BUILD_TARGETS.forEach(({ arch }) => { const debArch = getDebPackageArch(arch); - const prepareDebTask = task.define(`vscode-linux-${arch}-prepare-deb`, task.series(util.rimraf(`.build/linux/deb/${debArch}`), prepareDebPackage(arch))); + const prepareDebTask = task.define(`vscode-linux-${arch}-prepare-deb`, task.series(rimraf(`.build/linux/deb/${debArch}`), prepareDebPackage(arch))); gulp.task(prepareDebTask); const buildDebTask = task.define(`vscode-linux-${arch}-build-deb`, buildDebPackage(arch)); gulp.task(buildDebTask); const rpmArch = getRpmPackageArch(arch); - const prepareRpmTask = task.define(`vscode-linux-${arch}-prepare-rpm`, task.series(util.rimraf(`.build/linux/rpm/${rpmArch}`), prepareRpmPackage(arch))); + const prepareRpmTask = task.define(`vscode-linux-${arch}-prepare-rpm`, task.series(rimraf(`.build/linux/rpm/${rpmArch}`), prepareRpmPackage(arch))); gulp.task(prepareRpmTask); const buildRpmTask = task.define(`vscode-linux-${arch}-build-rpm`, buildRpmPackage(arch)); gulp.task(buildRpmTask); - const prepareSnapTask = task.define(`vscode-linux-${arch}-prepare-snap`, task.series(util.rimraf(`.build/linux/snap/${arch}`), prepareSnapPackage(arch))); + const prepareSnapTask = task.define(`vscode-linux-${arch}-prepare-snap`, task.series(rimraf(`.build/linux/snap/${arch}`), prepareSnapPackage(arch))); gulp.task(prepareSnapTask); const buildSnapTask = task.define(`vscode-linux-${arch}-build-snap`, task.series(prepareSnapTask, buildSnapPackage(arch))); gulp.task(buildSnapTask); diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index 85129a523da..0424b12fa45 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -12,12 +12,12 @@ const util = require('./lib/util'); const { getVersion } = require('./lib/getVersion'); const task = require('./lib/task'); const optimize = require('./lib/optimize'); +const { readISODate } = require('./lib/date'); const product = require('../product.json'); const rename = require('gulp-rename'); const filter = require('gulp-filter'); const { getProductionDependencies } = require('./lib/dependencies'); const vfs = require('vinyl-fs'); -const replace = require('gulp-replace'); const packageJson = require('../package.json'); const { compileBuildTask } = require('./gulpfile.compile'); const extensions = require('./lib/extensions'); @@ -31,12 +31,16 @@ const quality = product.quality; const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version; const vscodeWebResourceIncludes = [ + // Workbench 'out-build/vs/{base,platform,editor,workbench}/**/*.{svg,png,jpg,mp3}', 'out-build/vs/code/browser/workbench/*.html', 'out-build/vs/base/browser/ui/codicons/codicon/**/*.ttf', 'out-build/vs/**/markdown.css', + // NLS + 'out-build/nls.messages.js', + // Webview 'out-build/vs/workbench/contrib/webview/browser/pre/*.js', 'out-build/vs/workbench/contrib/webview/browser/pre/*.html', @@ -70,14 +74,11 @@ const vscodeWebEntryPoints = [ buildfile.workerNotebook, buildfile.workerLanguageDetection, buildfile.workerLocalFileSearch, - buildfile.workerProfileAnalysis, buildfile.keyboardMaps, buildfile.workbenchWeb ].flat(); exports.vscodeWebEntryPoints = vscodeWebEntryPoints; -const buildDate = new Date().toISOString(); - /** * @param {object} product The parsed product.json file contents */ @@ -93,7 +94,7 @@ const createVSCodeWebProductConfigurationPatcher = (product) => { ...product, version, commit, - date: buildDate + date: readISODate('out-build') }); return content.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', () => productConfiguration.substr(1, productConfiguration.length - 2) /* without { and }*/); } @@ -175,7 +176,7 @@ const optimizeVSCodeWebTask = task.define('optimize-vscode-web', task.series( const minifyVSCodeWebTask = task.define('minify-vscode-web', task.series( optimizeVSCodeWebTask, util.rimraf('out-vscode-web-min'), - optimize.minifyTask('out-vscode-web', `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) + optimize.minifyTask('out-vscode-web', `https://main.vscode-cdn.net/sourcemaps/${commit}/core`) )); gulp.task(minifyVSCodeWebTask); diff --git a/build/lib/bundle.js b/build/lib/bundle.js index 61d9f015624..a0989638f7c 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -36,14 +36,10 @@ function bundle(entryPoints, config, callback) { const loader = loaderModule.exports; config.isBuild = true; config.paths = config.paths || {}; - if (!config.paths['vs/nls']) { - config.paths['vs/nls'] = 'out-build/vs/nls.build'; - } if (!config.paths['vs/css']) { config.paths['vs/css'] = 'out-build/vs/css.build'; } config.buildForceInvokeFactory = config.buildForceInvokeFactory || {}; - config.buildForceInvokeFactory['vs/nls'] = true; config.buildForceInvokeFactory['vs/css'] = true; loader.config(config); loader(['require'], (localRequire) => { @@ -53,7 +49,6 @@ function bundle(entryPoints, config, callback) { r += '.js'; } // avoid packaging the build version of plugins: - r = r.replace('vs/nls.build.js', 'vs/nls.js'); r = r.replace('vs/css.build.js', 'vs/css.js'); return { path: r, amdModuleId: entry.amdModuleId }; }; @@ -231,6 +226,9 @@ function removeDuplicateTSBoilerplate(destFiles) { { start: /^var __param/, end: /^};$/ }, { start: /^var __awaiter/, end: /^};$/ }, { start: /^var __generator/, end: /^};$/ }, + { start: /^var __createBinding/, end: /^}\)\);$/ }, + { start: /^var __setModuleDefault/, end: /^}\);$/ }, + { start: /^var __importStar/, end: /^};$/ }, ]; destFiles.forEach((destFile) => { const SEEN_BOILERPLATE = []; diff --git a/build/lib/bundle.ts b/build/lib/bundle.ts index c5fdc2da18c..692100ff515 100644 --- a/build/lib/bundle.ts +++ b/build/lib/bundle.ts @@ -138,14 +138,10 @@ export function bundle(entryPoints: IEntryPoint[], config: ILoaderConfig, callba const loader: any = loaderModule.exports; config.isBuild = true; config.paths = config.paths || {}; - if (!config.paths['vs/nls']) { - config.paths['vs/nls'] = 'out-build/vs/nls.build'; - } if (!config.paths['vs/css']) { config.paths['vs/css'] = 'out-build/vs/css.build'; } config.buildForceInvokeFactory = config.buildForceInvokeFactory || {}; - config.buildForceInvokeFactory['vs/nls'] = true; config.buildForceInvokeFactory['vs/css'] = true; loader.config(config); @@ -156,7 +152,6 @@ export function bundle(entryPoints: IEntryPoint[], config: ILoaderConfig, callba r += '.js'; } // avoid packaging the build version of plugins: - r = r.replace('vs/nls.build.js', 'vs/nls.js'); r = r.replace('vs/css.build.js', 'vs/css.js'); return { path: r, amdModuleId: entry.amdModuleId }; }; @@ -365,6 +360,9 @@ function removeDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { { start: /^var __param/, end: /^};$/ }, { start: /^var __awaiter/, end: /^};$/ }, { start: /^var __generator/, end: /^};$/ }, + { start: /^var __createBinding/, end: /^}\)\);$/ }, + { start: /^var __setModuleDefault/, end: /^}\);$/ }, + { start: /^var __importStar/, end: /^};$/ }, ]; destFiles.forEach((destFile) => { diff --git a/build/lib/compilation.js b/build/lib/compilation.js index b44cbefe78a..cafca34a0d8 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -41,7 +41,7 @@ function getTypeScriptCompilerOptions(src) { options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 0 : 1; return options; } -function createCompile(src, build, emitError, transpileOnly) { +function createCompile(src, { build, emitError, transpileOnly, preserveEnglish }) { const tsb = require('./tsb'); const sourcemaps = require('gulp-sourcemaps'); const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); @@ -71,7 +71,7 @@ function createCompile(src, build, emitError, transpileOnly) { .pipe(util.loadSourcemaps()) .pipe(compilation(token)) .pipe(noDeclarationsFilter) - .pipe(util.$if(build, nls.nls())) + .pipe(util.$if(build, nls.nls({ preserveEnglish }))) .pipe(noDeclarationsFilter.restore) .pipe(util.$if(!transpileOnly, sourcemaps.write('.', { addComment: false, @@ -90,7 +90,7 @@ function createCompile(src, build, emitError, transpileOnly) { } function transpileTask(src, out, swc) { const task = () => { - const transpile = createCompile(src, false, true, { swc }); + const transpile = createCompile(src, { build: false, emitError: true, transpileOnly: { swc }, preserveEnglish: false }); const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); return srcPipe .pipe(transpile()) @@ -104,7 +104,7 @@ function compileTask(src, out, build, options = {}) { if (os.totalmem() < 4_000_000_000) { throw new Error('compilation requires 4GB of RAM'); } - const compile = createCompile(src, build, true, false); + const compile = createCompile(src, { build, emitError: true, transpileOnly: false, preserveEnglish: !!options.preserveEnglish }); const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); const generator = new MonacoGenerator(false); if (src === 'src') { @@ -141,7 +141,7 @@ function compileTask(src, out, build, options = {}) { } function watchTask(out, build) { const task = () => { - const compile = createCompile('src', build, false, false); + const compile = createCompile('src', { build, emitError: false, transpileOnly: false, preserveEnglish: false }); const src = gulp.src('src/**', { base: 'src' }); const watchSrc = watch('src/**', { base: 'src', readDelay: 200 }); const generator = new MonacoGenerator(true); @@ -234,7 +234,7 @@ class MonacoGenerator { function generateApiProposalNames() { let eol; try { - const src = fs.readFileSync('src/vs/workbench/services/extensions/common/extensionsApiProposals.ts', 'utf-8'); + const src = fs.readFileSync('src/vs/platform/extensions/common/extensionsApiProposals.ts', 'utf-8'); const match = /\r?\n/m.exec(src); eol = match ? match[0] : os.EOL; } @@ -242,18 +242,27 @@ function generateApiProposalNames() { eol = os.EOL; } const pattern = /vscode\.proposed\.([a-zA-Z\d]+)\.d\.ts$/; - const proposalNames = new Set(); + const versionPattern = /^\s*\/\/\s*version\s*:\s*(\d+)\s*$/mi; + const proposals = new Map(); const input = es.through(); const output = input .pipe(util.filter((f) => pattern.test(f.path))) .pipe(es.through((f) => { const name = path.basename(f.path); const match = pattern.exec(name); - if (match) { - proposalNames.add(match[1]); + if (!match) { + return; } + const proposalName = match[1]; + const contents = f.contents.toString('utf8'); + const versionMatch = versionPattern.exec(contents); + const version = versionMatch ? versionMatch[1] : undefined; + proposals.set(proposalName, { + proposal: `https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${proposalName}.d.ts`, + version: version ? parseInt(version) : undefined + }); }, function () { - const names = [...proposalNames.values()].sort(); + const names = [...proposals.keys()].sort(); const contents = [ '/*---------------------------------------------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', @@ -262,14 +271,18 @@ function generateApiProposalNames() { '', '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', '', - 'export const allApiProposals = Object.freeze({', - `${names.map(name => `\t${name}: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${name}.d.ts'`).join(`,${eol}`)}`, - '});', - 'export type ApiProposalName = keyof typeof allApiProposals;', + 'const _allApiProposals = {', + `${names.map(proposalName => { + const proposal = proposals.get(proposalName); + return `\t${proposalName}: {${eol}\t\tproposal: '${proposal.proposal}',${eol}${proposal.version ? `\t\tversion: ${proposal.version}${eol}` : ''}\t}`; + }).join(`,${eol}`)}`, + '};', + 'export const allApiProposals = Object.freeze<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>(_allApiProposals);', + 'export type ApiProposalName = keyof typeof _allApiProposals;', '', ].join(eol); this.emit('data', new File({ - path: 'vs/workbench/services/extensions/common/extensionsApiProposals.ts', + path: 'vs/platform/extensions/common/extensionsApiProposals.ts', contents: Buffer.from(contents) })); this.emit('end'); diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index b88d0d29003..8c3614b4c13 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -42,7 +42,14 @@ function getTypeScriptCompilerOptions(src: string): ts.CompilerOptions { return options; } -function createCompile(src: string, build: boolean, emitError: boolean, transpileOnly: boolean | { swc: boolean }) { +interface ICompileTaskOptions { + readonly build: boolean; + readonly emitError: boolean; + readonly transpileOnly: boolean | { swc: boolean }; + readonly preserveEnglish: boolean; +} + +function createCompile(src: string, { build, emitError, transpileOnly, preserveEnglish }: ICompileTaskOptions) { const tsb = require('./tsb') as typeof import('./tsb'); const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); @@ -79,7 +86,7 @@ function createCompile(src: string, build: boolean, emitError: boolean, transpil .pipe(util.loadSourcemaps()) .pipe(compilation(token)) .pipe(noDeclarationsFilter) - .pipe(util.$if(build, nls.nls())) + .pipe(util.$if(build, nls.nls({ preserveEnglish }))) .pipe(noDeclarationsFilter.restore) .pipe(util.$if(!transpileOnly, sourcemaps.write('.', { addComment: false, @@ -102,7 +109,7 @@ export function transpileTask(src: string, out: string, swc: boolean): task.Stre const task = () => { - const transpile = createCompile(src, false, true, { swc }); + const transpile = createCompile(src, { build: false, emitError: true, transpileOnly: { swc }, preserveEnglish: false }); const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); return srcPipe @@ -114,7 +121,7 @@ export function transpileTask(src: string, out: string, swc: boolean): task.Stre return task; } -export function compileTask(src: string, out: string, build: boolean, options: { disableMangle?: boolean } = {}): task.StreamTask { +export function compileTask(src: string, out: string, build: boolean, options: { disableMangle?: boolean; preserveEnglish?: boolean } = {}): task.StreamTask { const task = () => { @@ -122,7 +129,7 @@ export function compileTask(src: string, out: string, build: boolean, options: { throw new Error('compilation requires 4GB of RAM'); } - const compile = createCompile(src, build, true, false); + const compile = createCompile(src, { build, emitError: true, transpileOnly: false, preserveEnglish: !!options.preserveEnglish }); const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); const generator = new MonacoGenerator(false); if (src === 'src') { @@ -166,7 +173,7 @@ export function compileTask(src: string, out: string, build: boolean, options: { export function watchTask(out: string, build: boolean): task.StreamTask { const task = () => { - const compile = createCompile('src', build, false, false); + const compile = createCompile('src', { build, emitError: false, transpileOnly: false, preserveEnglish: false }); const src = gulp.src('src/**', { base: 'src' }); const watchSrc = watch('src/**', { base: 'src', readDelay: 200 }); @@ -275,7 +282,7 @@ function generateApiProposalNames() { let eol: string; try { - const src = fs.readFileSync('src/vs/workbench/services/extensions/common/extensionsApiProposals.ts', 'utf-8'); + const src = fs.readFileSync('src/vs/platform/extensions/common/extensionsApiProposals.ts', 'utf-8'); const match = /\r?\n/m.exec(src); eol = match ? match[0] : os.EOL; } catch { @@ -283,7 +290,8 @@ function generateApiProposalNames() { } const pattern = /vscode\.proposed\.([a-zA-Z\d]+)\.d\.ts$/; - const proposalNames = new Set(); + const versionPattern = /^\s*\/\/\s*version\s*:\s*(\d+)\s*$/mi; + const proposals = new Map(); const input = es.through(); const output = input @@ -292,11 +300,22 @@ function generateApiProposalNames() { const name = path.basename(f.path); const match = pattern.exec(name); - if (match) { - proposalNames.add(match[1]); + if (!match) { + return; } + + const proposalName = match[1]; + + const contents = f.contents.toString('utf8'); + const versionMatch = versionPattern.exec(contents); + const version = versionMatch ? versionMatch[1] : undefined; + + proposals.set(proposalName, { + proposal: `https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${proposalName}.d.ts`, + version: version ? parseInt(version) : undefined + }); }, function () { - const names = [...proposalNames.values()].sort(); + const names = [...proposals.keys()].sort(); const contents = [ '/*---------------------------------------------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', @@ -305,15 +324,19 @@ function generateApiProposalNames() { '', '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', '', - 'export const allApiProposals = Object.freeze({', - `${names.map(name => `\t${name}: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${name}.d.ts'`).join(`,${eol}`)}`, - '});', - 'export type ApiProposalName = keyof typeof allApiProposals;', + 'const _allApiProposals = {', + `${names.map(proposalName => { + const proposal = proposals.get(proposalName)!; + return `\t${proposalName}: {${eol}\t\tproposal: '${proposal.proposal}',${eol}${proposal.version ? `\t\tversion: ${proposal.version}${eol}` : ''}\t}`; + }).join(`,${eol}`)}`, + '};', + 'export const allApiProposals = Object.freeze<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>(_allApiProposals);', + 'export type ApiProposalName = keyof typeof _allApiProposals;', '', ].join(eol); this.emit('data', new File({ - path: 'vs/workbench/services/extensions/common/extensionsApiProposals.ts', + path: 'vs/platform/extensions/common/extensionsApiProposals.ts', contents: Buffer.from(contents) })); this.emit('end'); diff --git a/build/lib/date.js b/build/lib/date.js new file mode 100644 index 00000000000..77fff0e5073 --- /dev/null +++ b/build/lib/date.js @@ -0,0 +1,32 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.writeISODate = writeISODate; +exports.readISODate = readISODate; +const path = require("path"); +const fs = require("fs"); +const root = path.join(__dirname, '..', '..'); +/** + * Writes a `outDir/date` file with the contents of the build + * so that other tasks during the build process can use it and + * all use the same date. + */ +function writeISODate(outDir) { + const result = () => new Promise((resolve, _) => { + const outDirectory = path.join(root, outDir); + fs.mkdirSync(outDirectory, { recursive: true }); + const date = new Date().toISOString(); + fs.writeFileSync(path.join(outDirectory, 'date'), date, 'utf8'); + resolve(); + }); + result.taskName = 'build-date-file'; + return result; +} +function readISODate(outDir) { + const outDirectory = path.join(root, outDir); + return fs.readFileSync(path.join(outDirectory, 'date'), 'utf8'); +} +//# sourceMappingURL=date.js.map \ No newline at end of file diff --git a/build/lib/date.ts b/build/lib/date.ts new file mode 100644 index 00000000000..998e89f8e6a --- /dev/null +++ b/build/lib/date.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as fs from 'fs'; + +const root = path.join(__dirname, '..', '..'); + +/** + * Writes a `outDir/date` file with the contents of the build + * so that other tasks during the build process can use it and + * all use the same date. + */ +export function writeISODate(outDir: string) { + const result = () => new Promise((resolve, _) => { + const outDirectory = path.join(root, outDir); + fs.mkdirSync(outDirectory, { recursive: true }); + + const date = new Date().toISOString(); + fs.writeFileSync(path.join(outDirectory, 'date'), date, 'utf8'); + + resolve(); + }); + result.taskName = 'build-date-file'; + return result; +} + +export function readISODate(outDir: string): string { + const outDirectory = path.join(root, outDir); + return fs.readFileSync(path.join(outDirectory, 'date'), 'utf8'); +} diff --git a/build/lib/electron.js b/build/lib/electron.js index 8524b18850c..99252e4e64a 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -54,7 +54,7 @@ function darwinBundleDocumentType(extensions, icon, nameOrSuffix, utis) { role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions, - iconFile: 'resources/darwin/' + icon + '.icns', + iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', utis }; } diff --git a/build/lib/electron.ts b/build/lib/electron.ts index ba93c3a2af3..7a2a2a19557 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -68,7 +68,7 @@ function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuff role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions, - iconFile: 'resources/darwin/' + icon + '.icns', + iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', utis }; } diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 6a6c0a7b4cd..58d4d3e9a7f 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -34,7 +34,7 @@ const getVersion_1 = require("./getVersion"); const fetch_1 = require("./fetch"); const root = path.dirname(path.dirname(__dirname)); const commit = (0, getVersion_1.getVersion)(root); -const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input) { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); return input diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 6edfdcb63fb..0582e0cb11e 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -28,7 +28,7 @@ import { fetchUrls, fetchGithub } from './fetch'; const root = path.dirname(path.dirname(__dirname)); const commit = getVersion(root); -const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input: Stream): Stream { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); diff --git a/build/lib/i18n.js b/build/lib/i18n.js index c33994987f0..a837cbc4ac0 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -23,6 +23,7 @@ const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); const iconv = require("@vscode/iconv-lite-umd"); const l10n_dev_1 = require("@vscode/l10n-dev"); +const REPO_ROOT_PATH = path.join(__dirname, '../..'); function log(message, ...rest) { fancyLog(ansiColors.green('[i18n]'), message, ...rest); } @@ -63,6 +64,17 @@ var BundledFormat; } BundledFormat.is = is; })(BundledFormat || (BundledFormat = {})); +var NLSKeysFormat; +(function (NLSKeysFormat) { + function is(value) { + if (value === undefined) { + return false; + } + const candidate = value; + return Array.isArray(candidate) && Array.isArray(candidate[1]); + } + NLSKeysFormat.is = is; +})(NLSKeysFormat || (NLSKeysFormat = {})); class Line { buffer = []; constructor(indent = 0) { @@ -265,67 +277,8 @@ function stripComments(content) { }); return result; } -function escapeCharacters(value) { - const result = []; - for (let i = 0; i < value.length; i++) { - const ch = value.charAt(i); - switch (ch) { - case '\'': - result.push('\\\''); - break; - case '"': - result.push('\\"'); - break; - case '\\': - result.push('\\\\'); - break; - case '\n': - result.push('\\n'); - break; - case '\r': - result.push('\\r'); - break; - case '\t': - result.push('\\t'); - break; - case '\b': - result.push('\\b'); - break; - case '\f': - result.push('\\f'); - break; - default: - result.push(ch); - } - } - return result.join(''); -} -function processCoreBundleFormat(fileHeader, languages, json, emitter) { - const keysSection = json.keys; - const messageSection = json.messages; - const bundleSection = json.bundles; - const statistics = Object.create(null); - const defaultMessages = Object.create(null); - const modules = Object.keys(keysSection); - modules.forEach((module) => { - const keys = keysSection[module]; - const messages = messageSection[module]; - if (!messages || keys.length !== messages.length) { - emitter.emit('error', `Message for module ${module} corrupted. Mismatch in number of keys and messages.`); - return; - } - const messageMap = Object.create(null); - defaultMessages[module] = messageMap; - keys.map((key, i) => { - if (typeof key === 'string') { - messageMap[key] = messages[i]; - } - else { - messageMap[key.key] = messages[i]; - } - }); - }); - const languageDirectory = path.join(__dirname, '..', '..', '..', 'vscode-loc', 'i18n'); +function processCoreBundleFormat(base, fileHeader, languages, json, emitter) { + const languageDirectory = path.join(REPO_ROOT_PATH, '..', 'vscode-loc', 'i18n'); if (!fs.existsSync(languageDirectory)) { log(`No VS Code localization repository found. Looking at ${languageDirectory}`); log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`); @@ -335,8 +288,6 @@ function processCoreBundleFormat(fileHeader, languages, json, emitter) { if (process.env['VSCODE_BUILD_VERBOSE']) { log(`Generating nls bundles for: ${language.id}`); } - statistics[language.id] = 0; - const localizedModules = Object.create(null); const languageFolderName = language.translationId || language.id; const i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); let allMessages; @@ -344,87 +295,36 @@ function processCoreBundleFormat(fileHeader, languages, json, emitter) { const content = stripComments(fs.readFileSync(i18nFile, 'utf8')); allMessages = JSON.parse(content); } - modules.forEach((module) => { - const order = keysSection[module]; - let moduleMessage; - if (allMessages) { - moduleMessage = allMessages.contents[module]; + let nlsIndex = 0; + const nlsResult = []; + for (const [moduleId, nlsKeys] of json) { + const moduleTranslations = allMessages?.contents[moduleId]; + for (const nlsKey of nlsKeys) { + nlsResult.push(moduleTranslations?.[nlsKey]); // pushing `undefined` is fine, as we keep english strings as fallback for monaco editor in the build + nlsIndex++; } - if (!moduleMessage) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`No localized messages found for module ${module}. Using default messages.`); - } - moduleMessage = defaultMessages[module]; - statistics[language.id] = statistics[language.id] + Object.keys(moduleMessage).length; - } - const localizedMessages = []; - order.forEach((keyInfo) => { - let key = null; - if (typeof keyInfo === 'string') { - key = keyInfo; - } - else { - key = keyInfo.key; - } - let message = moduleMessage[key]; - if (!message) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`No localized message found for key ${key} in module ${module}. Using default message.`); - } - message = defaultMessages[module][key]; - statistics[language.id] = statistics[language.id] + 1; - } - localizedMessages.push(message); - }); - localizedModules[module] = localizedMessages; - }); - Object.keys(bundleSection).forEach((bundle) => { - const modules = bundleSection[bundle]; - const contents = [ - fileHeader, - `define("${bundle}.nls.${language.id}", {` - ]; - modules.forEach((module, index) => { - contents.push(`\t"${module}": [`); - const messages = localizedModules[module]; - if (!messages) { - emitter.emit('error', `Didn't find messages for module ${module}.`); - return; - } - messages.forEach((message, index) => { - contents.push(`\t\t"${escapeCharacters(message)}${index < messages.length ? '",' : '"'}`); - }); - contents.push(index < modules.length - 1 ? '\t],' : '\t]'); - }); - contents.push('});'); - emitter.queue(new File({ path: bundle + '.nls.' + language.id + '.js', contents: Buffer.from(contents.join('\n'), 'utf-8') })); - }); - }); - Object.keys(statistics).forEach(key => { - const value = statistics[key]; - log(`${key} has ${value} untranslated strings.`); - }); - sortedLanguages.forEach(language => { - const stats = statistics[language.id]; - if (!stats) { - log(`\tNo translations found for language ${language.id}. Using default language instead.`); } + emitter.queue(new File({ + contents: Buffer.from(`${fileHeader} +globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(nlsResult)}; +globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), + base, + path: `${base}/nls.messages.${language.id}.js` + })); }); } function processNlsFiles(opts) { return (0, event_stream_1.through)(function (file) { const fileName = path.basename(file.path); - if (fileName === 'nls.metadata.json') { - let json = null; - if (file.isBuffer()) { - json = JSON.parse(file.contents.toString('utf8')); + if (fileName === 'bundleInfo.json') { // pick a root level file to put the core bundles + try { + const json = JSON.parse(fs.readFileSync(path.join(REPO_ROOT_PATH, opts.out, 'nls.keys.json')).toString()); + if (NLSKeysFormat.is(json)) { + processCoreBundleFormat(file.base, opts.fileHeader, opts.languages, json, this); + } } - else { - this.emit('error', `Failed to read component file: ${file.relative}`); - return; - } - if (BundledFormat.is(json)) { - processCoreBundleFormat(opts.fileHeader, opts.languages, json, this); + catch (error) { + this.emit('error', `Failed to read component file: ${error}`); } } this.queue(file); diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index 444e3abe59c..28f8cc993e6 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -16,6 +16,8 @@ import * as ansiColors from 'ansi-colors'; import * as iconv from '@vscode/iconv-lite-umd'; import { l10nJsonFormat, getL10nXlf, l10nJsonDetails, getL10nFilesFromXlf, getL10nJson } from '@vscode/l10n-dev'; +const REPO_ROOT_PATH = path.join(__dirname, '../..'); + function log(message: any, ...rest: any[]): void { fancyLog(ansiColors.green('[i18n]'), message, ...rest); } @@ -91,6 +93,19 @@ module BundledFormat { } } +type NLSKeysFormat = [string /* module ID */, string[] /* keys */]; + +module NLSKeysFormat { + export function is(value: any): value is NLSKeysFormat { + if (value === undefined) { + return false; + } + + const candidate = value as NLSKeysFormat; + return Array.isArray(candidate) && Array.isArray(candidate[1]); + } +} + interface BundledExtensionFormat { [key: string]: { messages: string[]; @@ -329,70 +344,8 @@ function stripComments(content: string): string { return result; } -function escapeCharacters(value: string): string { - const result: string[] = []; - for (let i = 0; i < value.length; i++) { - const ch = value.charAt(i); - switch (ch) { - case '\'': - result.push('\\\''); - break; - case '"': - result.push('\\"'); - break; - case '\\': - result.push('\\\\'); - break; - case '\n': - result.push('\\n'); - break; - case '\r': - result.push('\\r'); - break; - case '\t': - result.push('\\t'); - break; - case '\b': - result.push('\\b'); - break; - case '\f': - result.push('\\f'); - break; - default: - result.push(ch); - } - } - return result.join(''); -} - -function processCoreBundleFormat(fileHeader: string, languages: Language[], json: BundledFormat, emitter: ThroughStream) { - const keysSection = json.keys; - const messageSection = json.messages; - const bundleSection = json.bundles; - - const statistics: Record = Object.create(null); - - const defaultMessages: Record> = Object.create(null); - const modules = Object.keys(keysSection); - modules.forEach((module) => { - const keys = keysSection[module]; - const messages = messageSection[module]; - if (!messages || keys.length !== messages.length) { - emitter.emit('error', `Message for module ${module} corrupted. Mismatch in number of keys and messages.`); - return; - } - const messageMap: Record = Object.create(null); - defaultMessages[module] = messageMap; - keys.map((key, i) => { - if (typeof key === 'string') { - messageMap[key] = messages[i]; - } else { - messageMap[key.key] = messages[i]; - } - }); - }); - - const languageDirectory = path.join(__dirname, '..', '..', '..', 'vscode-loc', 'i18n'); +function processCoreBundleFormat(base: string, fileHeader: string, languages: Language[], json: NLSKeysFormat, emitter: ThroughStream) { + const languageDirectory = path.join(REPO_ROOT_PATH, '..', 'vscode-loc', 'i18n'); if (!fs.existsSync(languageDirectory)) { log(`No VS Code localization repository found. Looking at ${languageDirectory}`); log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`); @@ -403,8 +356,6 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json log(`Generating nls bundles for: ${language.id}`); } - statistics[language.id] = 0; - const localizedModules: Record = Object.create(null); const languageFolderName = language.translationId || language.id; const i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); let allMessages: I18nFormat | undefined; @@ -412,86 +363,38 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json const content = stripComments(fs.readFileSync(i18nFile, 'utf8')); allMessages = JSON.parse(content); } - modules.forEach((module) => { - const order = keysSection[module]; - let moduleMessage: { [messageKey: string]: string } | undefined; - if (allMessages) { - moduleMessage = allMessages.contents[module]; + + let nlsIndex = 0; + const nlsResult: Array = []; + for (const [moduleId, nlsKeys] of json) { + const moduleTranslations = allMessages?.contents[moduleId]; + for (const nlsKey of nlsKeys) { + nlsResult.push(moduleTranslations?.[nlsKey]); // pushing `undefined` is fine, as we keep english strings as fallback for monaco editor in the build + nlsIndex++; } - if (!moduleMessage) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`No localized messages found for module ${module}. Using default messages.`); - } - moduleMessage = defaultMessages[module]; - statistics[language.id] = statistics[language.id] + Object.keys(moduleMessage).length; - } - const localizedMessages: string[] = []; - order.forEach((keyInfo) => { - let key: string | null = null; - if (typeof keyInfo === 'string') { - key = keyInfo; - } else { - key = keyInfo.key; - } - let message: string = moduleMessage![key]; - if (!message) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`No localized message found for key ${key} in module ${module}. Using default message.`); - } - message = defaultMessages[module][key]; - statistics[language.id] = statistics[language.id] + 1; - } - localizedMessages.push(message); - }); - localizedModules[module] = localizedMessages; - }); - Object.keys(bundleSection).forEach((bundle) => { - const modules = bundleSection[bundle]; - const contents: string[] = [ - fileHeader, - `define("${bundle}.nls.${language.id}", {` - ]; - modules.forEach((module, index) => { - contents.push(`\t"${module}": [`); - const messages = localizedModules[module]; - if (!messages) { - emitter.emit('error', `Didn't find messages for module ${module}.`); - return; - } - messages.forEach((message, index) => { - contents.push(`\t\t"${escapeCharacters(message)}${index < messages.length ? '",' : '"'}`); - }); - contents.push(index < modules.length - 1 ? '\t],' : '\t]'); - }); - contents.push('});'); - emitter.queue(new File({ path: bundle + '.nls.' + language.id + '.js', contents: Buffer.from(contents.join('\n'), 'utf-8') })); - }); - }); - Object.keys(statistics).forEach(key => { - const value = statistics[key]; - log(`${key} has ${value} untranslated strings.`); - }); - sortedLanguages.forEach(language => { - const stats = statistics[language.id]; - if (!stats) { - log(`\tNo translations found for language ${language.id}. Using default language instead.`); } + + emitter.queue(new File({ + contents: Buffer.from(`${fileHeader} +globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(nlsResult)}; +globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), + base, + path: `${base}/nls.messages.${language.id}.js` + })); }); } -export function processNlsFiles(opts: { fileHeader: string; languages: Language[] }): ThroughStream { +export function processNlsFiles(opts: { out: string; fileHeader: string; languages: Language[] }): ThroughStream { return through(function (this: ThroughStream, file: File) { const fileName = path.basename(file.path); - if (fileName === 'nls.metadata.json') { - let json = null; - if (file.isBuffer()) { - json = JSON.parse((file.contents).toString('utf8')); - } else { - this.emit('error', `Failed to read component file: ${file.relative}`); - return; - } - if (BundledFormat.is(json)) { - processCoreBundleFormat(opts.fileHeader, opts.languages, json, this); + if (fileName === 'bundleInfo.json') { // pick a root level file to put the core bundles + try { + const json = JSON.parse(fs.readFileSync(path.join(REPO_ROOT_PATH, opts.out, 'nls.keys.json')).toString()); + if (NLSKeysFormat.is(json)) { + processCoreBundleFormat(file.base, opts.fileHeader, opts.languages, json, this); + } + } catch (error) { + this.emit('error', `Failed to read component file: ${error}`); } } this.queue(file); diff --git a/build/lib/inlineMeta.js b/build/lib/inlineMeta.js new file mode 100644 index 00000000000..f1dbfa83a7e --- /dev/null +++ b/build/lib/inlineMeta.js @@ -0,0 +1,48 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.inlineMeta = inlineMeta; +const es = require("event-stream"); +const path_1 = require("path"); +const packageJsonMarkerId = 'BUILD_INSERT_PACKAGE_CONFIGURATION'; +// TODO@bpasero in order to inline `product.json`, more work is +// needed to ensure that we cover all cases where modifications +// are done to the product configuration during build. There are +// at least 2 more changes that kick in very late: +// - a `darwinUniversalAssetId` is added in`create-universal-app.ts` +// - a `target` is added in `gulpfile.vscode.win32.js` +// const productJsonMarkerId = 'BUILD_INSERT_PRODUCT_CONFIGURATION'; +function inlineMeta(result, ctx) { + return result.pipe(es.through(function (file) { + if (matchesFile(file, ctx)) { + let content = file.contents.toString(); + let markerFound = false; + const packageMarker = `${packageJsonMarkerId}:"${packageJsonMarkerId}"`; // this needs to be the format after esbuild has processed the file (e.g. double quotes) + if (content.includes(packageMarker)) { + content = content.replace(packageMarker, JSON.stringify(JSON.parse(ctx.packageJsonFn())).slice(1, -1) /* trim braces */); + markerFound = true; + } + // const productMarker = `${productJsonMarkerId}:"${productJsonMarkerId}"`; // this needs to be the format after esbuild has processed the file (e.g. double quotes) + // if (content.includes(productMarker)) { + // content = content.replace(productMarker, JSON.stringify(JSON.parse(ctx.productJsonFn())).slice(1, -1) /* trim braces */); + // markerFound = true; + // } + if (markerFound) { + file.contents = Buffer.from(content); + } + } + this.emit('data', file); + })); +} +function matchesFile(file, ctx) { + for (const targetPath of ctx.targetPaths) { + if (file.basename === (0, path_1.basename)(targetPath)) { // TODO would be nicer to figure out root relative path to not match on false positives + return true; + } + } + return false; +} +//# sourceMappingURL=inlineMeta.js.map \ No newline at end of file diff --git a/build/lib/inlineMeta.ts b/build/lib/inlineMeta.ts new file mode 100644 index 00000000000..ef3987fc32e --- /dev/null +++ b/build/lib/inlineMeta.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as es from 'event-stream'; +import { basename } from 'path'; +import * as File from 'vinyl'; + +export interface IInlineMetaContext { + readonly targetPaths: string[]; + readonly packageJsonFn: () => string; + readonly productJsonFn: () => string; +} + +const packageJsonMarkerId = 'BUILD_INSERT_PACKAGE_CONFIGURATION'; + +// TODO@bpasero in order to inline `product.json`, more work is +// needed to ensure that we cover all cases where modifications +// are done to the product configuration during build. There are +// at least 2 more changes that kick in very late: +// - a `darwinUniversalAssetId` is added in`create-universal-app.ts` +// - a `target` is added in `gulpfile.vscode.win32.js` +// const productJsonMarkerId = 'BUILD_INSERT_PRODUCT_CONFIGURATION'; + +export function inlineMeta(result: NodeJS.ReadWriteStream, ctx: IInlineMetaContext): NodeJS.ReadWriteStream { + return result.pipe(es.through(function (file: File) { + if (matchesFile(file, ctx)) { + let content = file.contents.toString(); + let markerFound = false; + + const packageMarker = `${packageJsonMarkerId}:"${packageJsonMarkerId}"`; // this needs to be the format after esbuild has processed the file (e.g. double quotes) + if (content.includes(packageMarker)) { + content = content.replace(packageMarker, JSON.stringify(JSON.parse(ctx.packageJsonFn())).slice(1, -1) /* trim braces */); + markerFound = true; + } + + // const productMarker = `${productJsonMarkerId}:"${productJsonMarkerId}"`; // this needs to be the format after esbuild has processed the file (e.g. double quotes) + // if (content.includes(productMarker)) { + // content = content.replace(productMarker, JSON.stringify(JSON.parse(ctx.productJsonFn())).slice(1, -1) /* trim braces */); + // markerFound = true; + // } + + if (markerFound) { + file.contents = Buffer.from(content); + } + } + + this.emit('data', file); + })); +} + +function matchesFile(file: File, ctx: IInlineMetaContext): boolean { + for (const targetPath of ctx.targetPaths) { + if (file.basename === basename(targetPath)) { // TODO would be nicer to figure out root relative path to not match on false positives + return true; + } + } + return false; +} diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index dce2b85d658..7494b71bb66 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -68,7 +68,8 @@ const CORE_TYPES = [ 'fetch', 'RequestInit', 'Headers', - 'Response' + 'Response', + '__global' ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser @@ -170,59 +171,17 @@ const RULES = [ '@types/node' // no node.js ] }, - // Common: vs/workbench/api/common/extHostTypes.ts + // Common: vs/base/parts/sandbox/electron-sandbox/preload.js { - target: '**/vs/workbench/api/common/extHostTypes.ts', + target: '**/vs/base/parts/sandbox/electron-sandbox/preload.js', allowedTypes: [ ...CORE_TYPES, - // Safe access to global - '__global' + // Safe access to a very small subset of node.js + 'process', + 'NodeJS' ], disallowedTypes: NATIVE_TYPES, disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - // Common: vs/workbench/api/common/extHostChatAgents2.ts - { - target: '**/vs/workbench/api/common/extHostChatAgents2.ts', - allowedTypes: [ - ...CORE_TYPES, - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - // Common: vs/workbench/api/common/extHostChatVariables.ts - { - target: '**/vs/workbench/api/common/extHostChatVariables.ts', - allowedTypes: [ - ...CORE_TYPES, - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - // Common: vs/workbench/api/common/extensionHostMain.ts - { - target: '**/vs/workbench/api/common/extensionHostMain.ts', - allowedTypes: [ - ...CORE_TYPES, - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM '@types/node' // no node.js ] }, diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 039f222135d..4861fa6d86e 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -69,7 +69,8 @@ const CORE_TYPES = [ 'fetch', 'RequestInit', 'Headers', - 'Response' + 'Response', + '__global' ]; // Types that are defined in a common layer but are known to be only @@ -185,66 +186,18 @@ const RULES: IRule[] = [ ] }, - // Common: vs/workbench/api/common/extHostTypes.ts + // Common: vs/base/parts/sandbox/electron-sandbox/preload.js { - target: '**/vs/workbench/api/common/extHostTypes.ts', + target: '**/vs/base/parts/sandbox/electron-sandbox/preload.js', allowedTypes: [ ...CORE_TYPES, - // Safe access to global - '__global' + // Safe access to a very small subset of node.js + 'process', + 'NodeJS' ], disallowedTypes: NATIVE_TYPES, disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - - // Common: vs/workbench/api/common/extHostChatAgents2.ts - { - target: '**/vs/workbench/api/common/extHostChatAgents2.ts', - allowedTypes: [ - ...CORE_TYPES, - - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - - // Common: vs/workbench/api/common/extHostChatVariables.ts - { - target: '**/vs/workbench/api/common/extHostChatVariables.ts', - allowedTypes: [ - ...CORE_TYPES, - - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM - '@types/node' // no node.js - ] - }, - - // Common: vs/workbench/api/common/extensionHostMain.ts - { - target: '**/vs/workbench/api/common/extensionHostMain.ts', - allowedTypes: [ - ...CORE_TYPES, - - // Safe access to global - '__global' - ], - disallowedTypes: NATIVE_TYPES, - disallowedDefinitions: [ - 'lib.dom.d.ts', // no DOM '@types/node' // no node.js ] }, diff --git a/build/lib/mangle/index.js b/build/lib/mangle/index.js index bb6b414e845..10e1aeb0d5a 100644 --- a/build/lib/mangle/index.js +++ b/build/lib/mangle/index.js @@ -250,7 +250,6 @@ function isNameTakenInFile(node, name) { const skippedExportMangledFiles = [ // Build 'css.build', - 'nls.build', // Monaco 'editorCommon', 'editorOptions', diff --git a/build/lib/mangle/index.ts b/build/lib/mangle/index.ts index 4a7544f162b..a148c4dd637 100644 --- a/build/lib/mangle/index.ts +++ b/build/lib/mangle/index.ts @@ -283,7 +283,6 @@ function isNameTakenInFile(node: ts.Node, name: string): boolean { const skippedExportMangledFiles = [ // Build 'css.build', - 'nls.build', // Monaco 'editorCommon', diff --git a/build/lib/nls.js b/build/lib/nls.js index 48ca84f2433..ae235a5a534 100644 --- a/build/lib/nls.js +++ b/build/lib/nls.js @@ -38,21 +38,11 @@ function clone(object) { } return result; } -function template(lines) { - let indent = '', wrap = ''; - if (lines.length > 1) { - indent = '\t'; - wrap = '\n'; - } - return `/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ -define([], [${wrap + lines.map(l => indent + l).join(',\n') + wrap}]);`; -} /** * Returns a stream containing the patched JavaScript and source maps. */ -function nls() { +function nls(options) { + let base; const input = (0, event_stream_1.through)(); const output = input.pipe((0, event_stream_1.through)(function (f) { if (!f.sourceMap) { @@ -70,7 +60,40 @@ function nls() { if (!typescript) { return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); } - _nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); + base = f.base; + this.emit('data', _nls.patchFile(f, typescript, options)); + }, function () { + for (const file of [ + new File({ + contents: Buffer.from(JSON.stringify({ + keys: _nls.moduleToNLSKeys, + messages: _nls.moduleToNLSMessages, + }, null, '\t')), + base, + path: `${base}/nls.metadata.json` + }), + new File({ + contents: Buffer.from(JSON.stringify(_nls.allNLSMessages)), + base, + path: `${base}/nls.messages.json` + }), + new File({ + contents: Buffer.from(JSON.stringify(_nls.allNLSModulesAndKeys)), + base, + path: `${base}/nls.keys.json` + }), + new File({ + contents: Buffer.from(`/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ +globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(_nls.allNLSMessages)};`), + base, + path: `${base}/nls.messages.js` + }) + ]) { + this.emit('data', file); + } + this.emit('end'); })); return (0, event_stream_1.duplex)(input, output); } @@ -79,6 +102,11 @@ function isImportNode(ts, node) { } var _nls; (function (_nls) { + _nls.moduleToNLSKeys = {}; + _nls.moduleToNLSMessages = {}; + _nls.allNLSMessages = []; + _nls.allNLSModulesAndKeys = []; + let allNLSMessagesIndex = 0; function fileFrom(file, contents, path = file.path) { return new File({ contents: Buffer.from(contents), @@ -146,13 +174,6 @@ var _nls; .filter(d => d.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) .filter(d => d.moduleSpecifier.getText() === '\'vs/nls\'') .filter(d => !!d.importClause && !!d.importClause.namedBindings); - const nlsExpressions = importEqualsDeclarations - .map(d => d.moduleReference.expression) - .concat(importDeclarations.map(d => d.moduleSpecifier)) - .map(d => ({ - start: ts.getLineAndCharacterOfPosition(sourceFile, d.getStart()), - end: ts.getLineAndCharacterOfPosition(sourceFile, d.getEnd()) - })); // `nls.localize(...)` calls const nlsLocalizeCallExpressions = importDeclarations .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport)) @@ -206,8 +227,7 @@ var _nls; value: a[1].getText() })); return { - localizeCalls: localizeCalls.toArray(), - nlsExpressions: nlsExpressions.toArray() + localizeCalls: localizeCalls.toArray() }; } class TextModel { @@ -262,14 +282,10 @@ var _nls; .flatten().toArray().join(''); } } - function patchJavascript(patches, contents, moduleId) { + function patchJavascript(patches, contents) { const model = new TextModel(contents); // patch the localize calls lazy(patches).reverse().each(p => model.apply(p)); - // patch the 'vs/nls' imports - const firstLine = model.get(0); - const patchedFirstLine = firstLine.replace(/(['"])vs\/nls\1/g, `$1vs/nls!${moduleId}$1`); - model.set(0, patchedFirstLine); return model.toString(); } function patchSourcemap(patches, rsm, smc) { @@ -307,14 +323,21 @@ var _nls; } return JSON.parse(smg.toString()); } - function patch(ts, moduleId, typescript, javascript, sourcemap) { - const { localizeCalls, nlsExpressions } = analyze(ts, typescript, 'localize'); - const { localizeCalls: localize2Calls, nlsExpressions: nls2Expressions } = analyze(ts, typescript, 'localize2'); + function parseLocalizeKeyOrValue(sourceExpression) { + // sourceValue can be "foo", 'foo', `foo` or { .... } + // in its evalulated form + // we want to return either the string or the object + // eslint-disable-next-line no-eval + return eval(`(${sourceExpression})`); + } + function patch(ts, typescript, javascript, sourcemap, options) { + const { localizeCalls } = analyze(ts, typescript, 'localize'); + const { localizeCalls: localize2Calls } = analyze(ts, typescript, 'localize2'); if (localizeCalls.length === 0 && localize2Calls.length === 0) { return { javascript, sourcemap }; } - const nlsKeys = template(localizeCalls.map(lc => lc.key).concat(localize2Calls.map(lc => lc.key))); - const nls = template(localizeCalls.map(lc => lc.value).concat(localize2Calls.map(lc => lc.value))); + const nlsKeys = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.key)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.key))); + const nlsMessages = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.value)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.value))); const smc = new sm.SourceMapConsumer(sourcemap); const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]); // build patches @@ -323,16 +346,18 @@ var _nls; const end = lcFrom(smc.generatedPositionFor(positionFrom(c.range.end))); return { span: { start, end }, content: c.content }; }; - let i = 0; const localizePatches = lazy(localizeCalls) - .map(lc => ([ - { range: lc.keySpan, content: '' + (i++) }, + .map(lc => (options.preserveEnglish ? [ + { range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize('key', "message") => localize(, "message") + ] : [ + { range: lc.keySpan, content: `${allNLSMessagesIndex++}` }, // localize('key', "message") => localize(, null) { range: lc.valueSpan, content: 'null' } ])) .flatten() .map(toPatch); const localize2Patches = lazy(localize2Calls) - .map(lc => ({ range: lc.keySpan, content: '' + (i++) })) + .map(lc => ({ range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize2('key', "message") => localize(, "message") + )) .map(toPatch); // Sort patches by their start position const patches = localizePatches.concat(localize2Patches).toArray().sort((a, b) => { @@ -352,34 +377,29 @@ var _nls; return 0; } }); - javascript = patchJavascript(patches, javascript, moduleId); - // since imports are not within the sourcemap information, - // we must do this MacGyver style - if (nlsExpressions.length || nls2Expressions.length) { - javascript = javascript.replace(/^define\(.*$/m, line => { - return line.replace(/(['"])vs\/nls\1/g, `$1vs/nls!${moduleId}$1`); - }); - } + javascript = patchJavascript(patches, javascript); sourcemap = patchSourcemap(patches, sourcemap, smc); - return { javascript, sourcemap, nlsKeys, nls }; + return { javascript, sourcemap, nlsKeys, nlsMessages }; } - function patchFiles(javascriptFile, typescript) { + function patchFile(javascriptFile, typescript, options) { const ts = require('typescript'); // hack? const moduleId = javascriptFile.relative .replace(/\.js$/, '') .replace(/\\/g, '/'); - const { javascript, sourcemap, nlsKeys, nls } = patch(ts, moduleId, typescript, javascriptFile.contents.toString(), javascriptFile.sourceMap); - const result = [fileFrom(javascriptFile, javascript)]; - result[0].sourceMap = sourcemap; + const { javascript, sourcemap, nlsKeys, nlsMessages } = patch(ts, typescript, javascriptFile.contents.toString(), javascriptFile.sourceMap, options); + const result = fileFrom(javascriptFile, javascript); + result.sourceMap = sourcemap; if (nlsKeys) { - result.push(fileFrom(javascriptFile, nlsKeys, javascriptFile.path.replace(/\.js$/, '.nls.keys.js'))); + _nls.moduleToNLSKeys[moduleId] = nlsKeys; + _nls.allNLSModulesAndKeys.push([moduleId, nlsKeys.map(nlsKey => typeof nlsKey === 'string' ? nlsKey : nlsKey.key)]); } - if (nls) { - result.push(fileFrom(javascriptFile, nls, javascriptFile.path.replace(/\.js$/, '.nls.js'))); + if (nlsMessages) { + _nls.moduleToNLSMessages[moduleId] = nlsMessages; + _nls.allNLSMessages.push(...nlsMessages); } return result; } - _nls.patchFiles = patchFiles; + _nls.patchFile = patchFile; })(_nls || (_nls = {})); //# sourceMappingURL=nls.js.map \ No newline at end of file diff --git a/build/lib/nls.ts b/build/lib/nls.ts index c4ee031b2eb..066dc1440c2 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -48,24 +48,11 @@ function clone(object: T): T { return result; } -function template(lines: string[]): string { - let indent = '', wrap = ''; - - if (lines.length > 1) { - indent = '\t'; - wrap = '\n'; - } - - return `/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ -define([], [${wrap + lines.map(l => indent + l).join(',\n') + wrap}]);`; -} - /** * Returns a stream containing the patched JavaScript and source maps. */ -export function nls(): NodeJS.ReadWriteStream { +export function nls(options: { preserveEnglish: boolean }): NodeJS.ReadWriteStream { + let base: string; const input = through(); const output = input.pipe(through(function (f: FileSourceMap) { if (!f.sourceMap) { @@ -87,7 +74,41 @@ export function nls(): NodeJS.ReadWriteStream { return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); } - _nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); + base = f.base; + this.emit('data', _nls.patchFile(f, typescript, options)); + }, function () { + for (const file of [ + new File({ + contents: Buffer.from(JSON.stringify({ + keys: _nls.moduleToNLSKeys, + messages: _nls.moduleToNLSMessages, + }, null, '\t')), + base, + path: `${base}/nls.metadata.json` + }), + new File({ + contents: Buffer.from(JSON.stringify(_nls.allNLSMessages)), + base, + path: `${base}/nls.messages.json` + }), + new File({ + contents: Buffer.from(JSON.stringify(_nls.allNLSModulesAndKeys)), + base, + path: `${base}/nls.keys.json` + }), + new File({ + contents: Buffer.from(`/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ +globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(_nls.allNLSMessages)};`), + base, + path: `${base}/nls.messages.js` + }) + ]) { + this.emit('data', file); + } + + this.emit('end'); })); return duplex(input, output); @@ -99,11 +120,19 @@ function isImportNode(ts: typeof import('typescript'), node: ts.Node): boolean { module _nls { - interface INlsStringResult { + export const moduleToNLSKeys: { [name: string /* module ID */]: ILocalizeKey[] /* keys */ } = {}; + export const moduleToNLSMessages: { [name: string /* module ID */]: string[] /* messages */ } = {}; + export const allNLSMessages: string[] = []; + export const allNLSModulesAndKeys: Array<[string /* module ID */, string[] /* keys */]> = []; + let allNLSMessagesIndex = 0; + + type ILocalizeKey = string | { key: string }; // key might contain metadata for translators and then is not just a string + + interface INlsPatchResult { javascript: string; sourcemap: sm.RawSourceMap; - nls?: string; - nlsKeys?: string; + nlsMessages?: string[]; + nlsKeys?: ILocalizeKey[]; } interface ISpan { @@ -120,7 +149,6 @@ module _nls { interface ILocalizeAnalysisResult { localizeCalls: ILocalizeCall[]; - nlsExpressions: ISpan[]; } interface IPatch { @@ -210,14 +238,6 @@ module _nls { .filter(d => d.moduleSpecifier.getText() === '\'vs/nls\'') .filter(d => !!d.importClause && !!d.importClause.namedBindings); - const nlsExpressions = importEqualsDeclarations - .map(d => (d.moduleReference).expression) - .concat(importDeclarations.map(d => d.moduleSpecifier)) - .map(d => ({ - start: ts.getLineAndCharacterOfPosition(sourceFile, d.getStart()), - end: ts.getLineAndCharacterOfPosition(sourceFile, d.getEnd()) - })); - // `nls.localize(...)` calls const nlsLocalizeCallExpressions = importDeclarations .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport)) @@ -280,8 +300,7 @@ module _nls { })); return { - localizeCalls: localizeCalls.toArray(), - nlsExpressions: nlsExpressions.toArray() + localizeCalls: localizeCalls.toArray() }; } @@ -351,17 +370,12 @@ module _nls { } } - function patchJavascript(patches: IPatch[], contents: string, moduleId: string): string { + function patchJavascript(patches: IPatch[], contents: string): string { const model = new TextModel(contents); // patch the localize calls lazy(patches).reverse().each(p => model.apply(p)); - // patch the 'vs/nls' imports - const firstLine = model.get(0); - const patchedFirstLine = firstLine.replace(/(['"])vs\/nls\1/g, `$1vs/nls!${moduleId}$1`); - model.set(0, patchedFirstLine); - return model.toString(); } @@ -410,16 +424,24 @@ module _nls { return JSON.parse(smg.toString()); } - function patch(ts: typeof import('typescript'), moduleId: string, typescript: string, javascript: string, sourcemap: sm.RawSourceMap): INlsStringResult { - const { localizeCalls, nlsExpressions } = analyze(ts, typescript, 'localize'); - const { localizeCalls: localize2Calls, nlsExpressions: nls2Expressions } = analyze(ts, typescript, 'localize2'); + function parseLocalizeKeyOrValue(sourceExpression: string) { + // sourceValue can be "foo", 'foo', `foo` or { .... } + // in its evalulated form + // we want to return either the string or the object + // eslint-disable-next-line no-eval + return eval(`(${sourceExpression})`); + } + + function patch(ts: typeof import('typescript'), typescript: string, javascript: string, sourcemap: sm.RawSourceMap, options: { preserveEnglish: boolean }): INlsPatchResult { + const { localizeCalls } = analyze(ts, typescript, 'localize'); + const { localizeCalls: localize2Calls } = analyze(ts, typescript, 'localize2'); if (localizeCalls.length === 0 && localize2Calls.length === 0) { return { javascript, sourcemap }; } - const nlsKeys = template(localizeCalls.map(lc => lc.key).concat(localize2Calls.map(lc => lc.key))); - const nls = template(localizeCalls.map(lc => lc.value).concat(localize2Calls.map(lc => lc.value))); + const nlsKeys = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.key)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.key))); + const nlsMessages = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.value)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.value))); const smc = new sm.SourceMapConsumer(sourcemap); const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]); @@ -430,18 +452,20 @@ module _nls { return { span: { start, end }, content: c.content }; }; - let i = 0; const localizePatches = lazy(localizeCalls) - .map(lc => ([ - { range: lc.keySpan, content: '' + (i++) }, - { range: lc.valueSpan, content: 'null' } - ])) + .map(lc => ( + options.preserveEnglish ? [ + { range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize('key', "message") => localize(, "message") + ] : [ + { range: lc.keySpan, content: `${allNLSMessagesIndex++}` }, // localize('key', "message") => localize(, null) + { range: lc.valueSpan, content: 'null' } + ])) .flatten() .map(toPatch); const localize2Patches = lazy(localize2Calls) .map(lc => ( - { range: lc.keySpan, content: '' + (i++) } + { range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize2('key', "message") => localize(, "message") )) .map(toPatch); @@ -460,45 +484,39 @@ module _nls { } }); - javascript = patchJavascript(patches, javascript, moduleId); - - // since imports are not within the sourcemap information, - // we must do this MacGyver style - if (nlsExpressions.length || nls2Expressions.length) { - javascript = javascript.replace(/^define\(.*$/m, line => { - return line.replace(/(['"])vs\/nls\1/g, `$1vs/nls!${moduleId}$1`); - }); - } + javascript = patchJavascript(patches, javascript); sourcemap = patchSourcemap(patches, sourcemap, smc); - return { javascript, sourcemap, nlsKeys, nls }; + return { javascript, sourcemap, nlsKeys, nlsMessages }; } - export function patchFiles(javascriptFile: File, typescript: string): File[] { + export function patchFile(javascriptFile: File, typescript: string, options: { preserveEnglish: boolean }): File { const ts = require('typescript') as typeof import('typescript'); // hack? const moduleId = javascriptFile.relative .replace(/\.js$/, '') .replace(/\\/g, '/'); - const { javascript, sourcemap, nlsKeys, nls } = patch( + const { javascript, sourcemap, nlsKeys, nlsMessages } = patch( ts, - moduleId, typescript, javascriptFile.contents.toString(), - (javascriptFile).sourceMap + (javascriptFile).sourceMap, + options ); - const result: File[] = [fileFrom(javascriptFile, javascript)]; - (result[0]).sourceMap = sourcemap; + const result = fileFrom(javascriptFile, javascript); + (result).sourceMap = sourcemap; if (nlsKeys) { - result.push(fileFrom(javascriptFile, nlsKeys, javascriptFile.path.replace(/\.js$/, '.nls.keys.js'))); + moduleToNLSKeys[moduleId] = nlsKeys; + allNLSModulesAndKeys.push([moduleId, nlsKeys.map(nlsKey => typeof nlsKey === 'string' ? nlsKey : nlsKey.key)]); } - if (nls) { - result.push(fileFrom(javascriptFile, nls, javascriptFile.path.replace(/\.js$/, '.nls.js'))); + if (nlsMessages) { + moduleToNLSMessages[moduleId] = nlsMessages; + allNLSMessages.push(...nlsMessages); } return result; diff --git a/build/lib/optimize.js b/build/lib/optimize.js index d48235ebf15..79509e9a2d5 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -53,7 +53,7 @@ function loaderPlugin(src, base, amdModuleId) { function loader(src, bundledFileHeader, bundleLoader, externalLoaderInfo) { let loaderStream = gulp.src(`${src}/vs/loader.js`, { base: `${src}` }); if (bundleLoader) { - loaderStream = es.merge(loaderStream, loaderPlugin(`${src}/vs/css.js`, `${src}`, 'vs/css'), loaderPlugin(`${src}/vs/nls.js`, `${src}`, 'vs/nls')); + loaderStream = es.merge(loaderStream, loaderPlugin(`${src}/vs/css.js`, `${src}`, 'vs/css')); } const files = []; const order = (f) => { @@ -63,10 +63,7 @@ function loader(src, bundledFileHeader, bundleLoader, externalLoaderInfo) { if (f.path.endsWith('css.js')) { return 1; } - if (f.path.endsWith('nls.js')) { - return 2; - } - return 3; + return 2; }; return (loaderStream .pipe(es.through(function (data) { @@ -192,6 +189,7 @@ function optimizeAMDTask(opts) { includeContent: true })) .pipe(opts.languages && opts.languages.length ? (0, i18n_1.processNlsFiles)({ + out: opts.src, fileHeader: bundledFileHeader, languages: opts.languages }) : es.through()); diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index 5b6dee9bf65..6f9786b4d1e 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -60,8 +60,7 @@ function loader(src: string, bundledFileHeader: string, bundleLoader: boolean, e if (bundleLoader) { loaderStream = es.merge( loaderStream, - loaderPlugin(`${src}/vs/css.js`, `${src}`, 'vs/css'), - loaderPlugin(`${src}/vs/nls.js`, `${src}`, 'vs/nls'), + loaderPlugin(`${src}/vs/css.js`, `${src}`, 'vs/css') ); } @@ -73,10 +72,7 @@ function loader(src: string, bundledFileHeader: string, bundleLoader: boolean, e if (f.path.endsWith('css.js')) { return 1; } - if (f.path.endsWith('nls.js')) { - return 2; - } - return 3; + return 2; }; return ( @@ -269,6 +265,7 @@ function optimizeAMDTask(opts: IOptimizeAMDTaskOpts): NodeJS.ReadWriteStream { includeContent: true })) .pipe(opts.languages && opts.languages.length ? processNlsFiles({ + out: opts.src, fileHeader: bundledFileHeader, languages: opts.languages }) : es.through()); diff --git a/build/lib/standalone.js b/build/lib/standalone.js index dbc47db0833..cf0e452aff3 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -106,10 +106,7 @@ function extractEditor(options) { 'vs/css.build.ts', 'vs/css.ts', 'vs/loader.js', - 'vs/loader.d.ts', - 'vs/nls.build.ts', - 'vs/nls.ts', - 'vs/nls.mock.ts', + 'vs/loader.d.ts' ].forEach(copyFile); } function createESMSourcesAndResources2(options) { diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index 775a1be5996..9a65bfa7444 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -118,10 +118,7 @@ export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: str 'vs/css.build.ts', 'vs/css.ts', 'vs/loader.js', - 'vs/loader.d.ts', - 'vs/nls.build.ts', - 'vs/nls.ts', - 'vs/nls.mock.ts', + 'vs/loader.d.ts' ].forEach(copyFile); } diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index e6291132d56..148aa2786dd 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -94,6 +94,7 @@ "--vscode-debugTokenExpression-name", "--vscode-debugTokenExpression-number", "--vscode-debugTokenExpression-string", + "--vscode-debugTokenExpression-type", "--vscode-debugTokenExpression-value", "--vscode-debugToolBar-background", "--vscode-debugToolBar-border", @@ -129,12 +130,15 @@ "--vscode-editor-background", "--vscode-editor-findMatchBackground", "--vscode-editor-findMatchBorder", + "--vscode-editor-findMatchForeground", "--vscode-editor-findMatchHighlightBackground", "--vscode-editor-findMatchHighlightBorder", + "--vscode-editor-findMatchHighlightForeground", "--vscode-editor-findRangeHighlightBackground", "--vscode-editor-findRangeHighlightBorder", "--vscode-editor-focusedStackFrameHighlightBackground", "--vscode-editor-foldBackground", + "--vscode-editor-foldPlaceholderForeground", "--vscode-editor-foreground", "--vscode-editor-hoverHighlightBackground", "--vscode-editor-inactiveSelectionBackground", @@ -143,6 +147,7 @@ "--vscode-editor-lineHighlightBackground", "--vscode-editor-lineHighlightBorder", "--vscode-editor-linkedEditingBackground", + "--vscode-editor-placeholder-foreground", "--vscode-editor-rangeHighlightBackground", "--vscode-editor-rangeHighlightBorder", "--vscode-editor-selectionBackground", @@ -488,12 +493,12 @@ "--vscode-panelSectionHeader-background", "--vscode-panelSectionHeader-border", "--vscode-panelSectionHeader-foreground", - "--vscode-panelTitle-activeBorder", - "--vscode-panelTitle-activeForeground", - "--vscode-panelTitle-inactiveForeground", "--vscode-panelStickyScroll-background", "--vscode-panelStickyScroll-border", "--vscode-panelStickyScroll-shadow", + "--vscode-panelTitle-activeBorder", + "--vscode-panelTitle-activeForeground", + "--vscode-panelTitle-inactiveForeground", "--vscode-peekView-border", "--vscode-peekViewEditor-background", "--vscode-peekViewEditor-matchHighlightBackground", @@ -517,6 +522,7 @@ "--vscode-problemsWarningIcon-foreground", "--vscode-profileBadge-background", "--vscode-profileBadge-foreground", + "--vscode-profiles-sashBorder", "--vscode-progressBar-background", "--vscode-quickInput-background", "--vscode-quickInput-foreground", @@ -568,11 +574,11 @@ "--vscode-sideBarSectionHeader-background", "--vscode-sideBarSectionHeader-border", "--vscode-sideBarSectionHeader-foreground", - "--vscode-sideBarTitle-background", - "--vscode-sideBarTitle-foreground", "--vscode-sideBarStickyScroll-background", "--vscode-sideBarStickyScroll-border", "--vscode-sideBarStickyScroll-shadow", + "--vscode-sideBarTitle-background", + "--vscode-sideBarTitle-foreground", "--vscode-sideBySideEditor-horizontalBorder", "--vscode-sideBySideEditor-verticalBorder", "--vscode-simpleFindWidget-sashBorder", @@ -647,9 +653,6 @@ "--vscode-tab-activeBackground", "--vscode-tab-activeBorder", "--vscode-tab-activeBorderTop", - "--vscode-tab-selectedBorderTop", - "--vscode-tab-selectedBackground", - "--vscode-tab-selectedForeground", "--vscode-tab-activeForeground", "--vscode-tab-activeModifiedBorder", "--vscode-tab-border", @@ -661,6 +664,9 @@ "--vscode-tab-inactiveForeground", "--vscode-tab-inactiveModifiedBorder", "--vscode-tab-lastPinnedBorder", + "--vscode-tab-selectedBackground", + "--vscode-tab-selectedBorderTop", + "--vscode-tab-selectedForeground", "--vscode-tab-unfocusedActiveBackground", "--vscode-tab-unfocusedActiveBorder", "--vscode-tab-unfocusedActiveBorderTop", @@ -698,6 +704,7 @@ "--vscode-terminal-foreground", "--vscode-terminal-hoverHighlightBackground", "--vscode-terminal-inactiveSelectionBackground", + "--vscode-terminal-initialHintForeground", "--vscode-terminal-selectionBackground", "--vscode-terminal-selectionForeground", "--vscode-terminal-tab-activeBorder", @@ -709,6 +716,7 @@ "--vscode-terminalOverviewRuler-cursorForeground", "--vscode-terminalOverviewRuler-findMatchForeground", "--vscode-terminalStickyScroll-background", + "--vscode-terminalStickyScroll-border", "--vscode-terminalStickyScrollHover-background", "--vscode-testing-coverCountBadgeBackground", "--vscode-testing-coverCountBadgeForeground", @@ -813,8 +821,6 @@ "--vscode-hover-maxWidth", "--vscode-hover-sourceWhiteSpace", "--vscode-hover-whiteSpace", - "--vscode-inline-chat-quick-voice-height", - "--vscode-inline-chat-quick-voice-width", "--vscode-editor-dictation-widget-height", "--vscode-editor-dictation-widget-width", "--vscode-interactive-session-foreground", @@ -848,6 +854,8 @@ "--z-index-notebook-scrollbar", "--z-index-run-button-container", "--zoom-factor", - "--test-bar-width" + "--test-bar-width", + "--widget-color", + "--text-link-decoration" ] } diff --git a/build/lib/tsb/transpiler.js b/build/lib/tsb/transpiler.js index afec9062692..5dcc4ca1ed3 100644 --- a/build/lib/tsb/transpiler.js +++ b/build/lib/tsb/transpiler.js @@ -305,7 +305,7 @@ class SwcTranspiler { }, module: { type: 'amd', - noInterop: true + noInterop: false }, minify: false, }; @@ -313,7 +313,7 @@ class SwcTranspiler { ...this._swcrcAmd, module: { type: 'commonjs', - importInterop: 'none' + importInterop: 'swc' } }; static _swcrcEsm = { diff --git a/build/lib/tsb/transpiler.ts b/build/lib/tsb/transpiler.ts index a546ea63316..cbc3d9e8eee 100644 --- a/build/lib/tsb/transpiler.ts +++ b/build/lib/tsb/transpiler.ts @@ -388,7 +388,7 @@ export class SwcTranspiler implements ITranspiler { }, module: { type: 'amd', - noInterop: true + noInterop: false }, minify: false, }; @@ -397,7 +397,7 @@ export class SwcTranspiler implements ITranspiler { ...this._swcrcAmd, module: { type: 'commonjs', - importInterop: 'none' + importInterop: 'swc' } }; diff --git a/build/npm/gyp/package.json b/build/npm/gyp/package.json index 3961e955a5f..a1564133a1e 100644 --- a/build/npm/gyp/package.json +++ b/build/npm/gyp/package.json @@ -4,7 +4,7 @@ "private": true, "license": "MIT", "devDependencies": { - "node-gyp": "^9.4.0" + "node-gyp": "^10.1.0" }, "scripts": {} } diff --git a/build/npm/gyp/yarn.lock b/build/npm/gyp/yarn.lock index 96d132e7943..a9bf901727e 100644 --- a/build/npm/gyp/yarn.lock +++ b/build/npm/gyp/yarn.lock @@ -14,10 +14,21 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" +"@npmcli/agent@^2.0.0": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@npmcli/agent/-/agent-2.2.2.tgz#967604918e62f620a648c7975461c9c9e74fc5d5" + integrity sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og== + dependencies: + agent-base "^7.1.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.1" + lru-cache "^10.0.1" + socks-proxy-agent "^8.0.3" + "@npmcli/fs@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-3.1.0.tgz#233d43a25a91d68c3a863ba0da6a3f00924a173e" - integrity sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w== + version "3.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-3.1.1.tgz#59cdaa5adca95d135fc00f2bb53f5771575ce726" + integrity sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg== dependencies: semver "^7.3.5" @@ -26,31 +37,17 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@tootallnate/once@2": +abbrev@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" - integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== -abbrev@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -agent-base@6, agent-base@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== +agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== dependencies: - debug "4" - -agentkeepalive@^4.2.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255" - integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg== - dependencies: - debug "^4.1.0" - depd "^2.0.0" - humanize-ms "^1.2.1" + debug "^4.3.4" aggregate-error@^3.0.0: version "3.1.0" @@ -82,32 +79,11 @@ ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -"aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - -are-we-there-yet@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" - integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - brace-expansion@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" @@ -115,17 +91,17 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -cacache@^17.0.0: - version "17.1.3" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.3.tgz#c6ac23bec56516a7c0c52020fd48b4909d7c7044" - integrity sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg== +cacache@^18.0.0: + version "18.0.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-18.0.3.tgz#864e2c18414e1e141ae8763f31e46c2cb96d1b21" + integrity sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg== dependencies: "@npmcli/fs" "^3.1.0" fs-minipass "^3.0.0" glob "^10.2.2" - lru-cache "^7.7.1" - minipass "^5.0.0" - minipass-collect "^1.0.2" + lru-cache "^10.0.1" + minipass "^7.0.3" + minipass-collect "^2.0.1" minipass-flush "^1.0.5" minipass-pipeline "^1.2.4" p-map "^4.0.0" @@ -155,21 +131,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -console-control-strings@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - cross-spawn@^7.0.0: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -179,22 +140,19 @@ cross-spawn@^7.0.0: shebang-command "^2.0.0" which "^2.0.1" -debug@4, debug@^4.1.0, debug@^4.3.3: +debug@4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== - -depd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" eastasianwidth@^0.2.0: version "0.2.0" @@ -234,9 +192,9 @@ exponential-backoff@^3.1.1: integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== foreground-child@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" - integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== dependencies: cross-spawn "^7.0.0" signal-exit "^4.0.1" @@ -249,93 +207,50 @@ fs-minipass@^2.0.0: minipass "^3.0.0" fs-minipass@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.2.tgz#5b383858efa8c1eb8c33b39e994f7e8555b8b3a3" - integrity sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g== + version "3.0.3" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.3.tgz#79a85981c4dc120065e96f62086bf6f9dc26cc54" + integrity sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw== dependencies: - minipass "^5.0.0" + minipass "^7.0.3" -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -gauge@^4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" - integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.3" - console-control-strings "^1.1.0" - has-unicode "^2.0.1" - signal-exit "^3.0.7" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.5" - -glob@^10.2.2: - version "10.3.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.3.tgz#8360a4ffdd6ed90df84aa8d52f21f452e86a123b" - integrity sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw== +glob@^10.2.2, glob@^10.3.10: + version "10.4.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.2.tgz#bed6b95dade5c1f80b4434daced233aee76160e5" + integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w== dependencies: foreground-child "^3.1.0" - jackspeak "^2.0.3" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" - -glob@^7.1.3, glob@^7.1.4: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" graceful-fs@^4.2.6: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - http-cache-semantics@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== -http-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" - integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== +http-proxy-agent@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" + agent-base "^7.1.0" + debug "^4.3.4" -https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== +https-proxy-agent@^7.0.1: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== dependencies: - agent-base "6" + agent-base "^7.0.2" debug "4" -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - iconv-lite@^0.6.2: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -353,23 +268,13 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ip@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105" - integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ== + jsbn "1.1.0" + sprintf-js "^1.1.3" is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -386,15 +291,30 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -jackspeak@^2.0.3: - version "2.2.2" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.2.tgz#707c62733924b8dc2a0a629dc6248577788b5385" - integrity sha512-mgNtVv4vUuaKA97yxUHoA3+FkuhtxkjdXEWOyB/N76fjy0FjezEt34oy3epBtvCvS+7DyKwqCFWx/oJLV5+kCg== +isexe@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" + integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== + +jackspeak@^3.1.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.0.tgz#a75763ff36ad778ede6a156d8ee8b124de445b4a" + integrity sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +lru-cache@^10.0.1, lru-cache@^10.2.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.3.0.tgz#4a4aaf10c84658ab70f79a85a9a3f1e1fb11196b" + integrity sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -402,64 +322,44 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^7.7.1: - version "7.18.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" - integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== - -"lru-cache@^9.1.1 || ^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61" - integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw== - -make-fetch-happen@^11.0.3: - version "11.1.1" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz#85ceb98079584a9523d4bf71d32996e7e208549f" - integrity sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w== +make-fetch-happen@^13.0.0: + version "13.0.1" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz#273ba2f78f45e1f3a6dca91cede87d9fa4821e36" + integrity sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA== dependencies: - agentkeepalive "^4.2.1" - cacache "^17.0.0" + "@npmcli/agent" "^2.0.0" + cacache "^18.0.0" http-cache-semantics "^4.1.1" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" is-lambda "^1.0.1" - lru-cache "^7.7.1" - minipass "^5.0.0" + minipass "^7.0.2" minipass-fetch "^3.0.0" minipass-flush "^1.0.5" minipass-pipeline "^1.2.4" negotiator "^0.6.3" + proc-log "^4.2.0" promise-retry "^2.0.1" - socks-proxy-agent "^7.0.0" ssri "^10.0.0" -minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^9.0.1: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" -minipass-collect@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" - integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== +minipass-collect@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-2.0.1.tgz#1621bc77e12258a12c60d34e2276ec5c20680863" + integrity sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw== dependencies: - minipass "^3.0.0" + minipass "^7.0.3" minipass-fetch@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-3.0.3.tgz#d9df70085609864331b533c960fd4ffaa78d15ce" - integrity sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ== + version "3.0.5" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-3.0.5.tgz#f0f97e40580affc4a35cc4a1349f05ae36cb1e4c" + integrity sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg== dependencies: - minipass "^5.0.0" + minipass "^7.0.3" minipass-sized "^1.0.3" minizlib "^2.1.2" optionalDependencies: @@ -498,10 +398,10 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.0.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.2.tgz#58a82b7d81c7010da5bd4b2c0c85ac4b4ec5131e" - integrity sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.2, minipass@^7.0.3, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" @@ -521,56 +421,33 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -node-gyp@^9.4.0: - version "9.4.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.0.tgz#2a7a91c7cba4eccfd95e949369f27c9ba704f369" - integrity sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg== +node-gyp@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-10.1.0.tgz#75e6f223f2acb4026866c26a2ead6aab75a8ca7e" + integrity sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA== dependencies: env-paths "^2.2.0" exponential-backoff "^3.1.1" - glob "^7.1.4" + glob "^10.3.10" graceful-fs "^4.2.6" - make-fetch-happen "^11.0.3" - nopt "^6.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" + make-fetch-happen "^13.0.0" + nopt "^7.0.0" + proc-log "^3.0.0" semver "^7.3.5" tar "^6.1.2" - which "^2.0.2" + which "^4.0.0" -nopt@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" - integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== +nopt@^7.0.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7" + integrity sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w== dependencies: - abbrev "^1.0.0" - -npmlog@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" - integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== - dependencies: - are-we-there-yet "^3.0.0" - console-control-strings "^1.1.0" - gauge "^4.0.3" - set-blocking "^2.0.0" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" + abbrev "^2.0.0" p-map@^4.0.0: version "4.0.0" @@ -579,24 +456,34 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-scurry@^1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: - lru-cache "^9.1.1 || ^10.0.0" + lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +proc-log@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8" + integrity sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A== + +proc-log@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-4.2.0.tgz#b6f461e4026e75fdfe228b265e9f7a00779d7034" + integrity sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA== + promise-retry@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" @@ -605,32 +492,11 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" -readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -643,11 +509,6 @@ semver@^7.3.5: dependencies: lru-cache "^6.0.0" -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -660,11 +521,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -675,31 +531,36 @@ smart-buffer@^4.2.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== -socks-proxy-agent@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" - integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== +socks-proxy-agent@^8.0.3: + version "8.0.4" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c" + integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" + agent-base "^7.1.1" + debug "^4.3.4" + socks "^2.8.3" -socks@^2.6.2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" - integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== +socks@^2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== dependencies: - ip "^2.0.0" + ip-address "^9.0.5" smart-buffer "^4.2.0" -ssri@^10.0.0: - version "10.0.4" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.4.tgz#5a20af378be586df139ddb2dfb3bf992cf0daba6" - integrity sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ== - dependencies: - minipass "^5.0.0" +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.3: +ssri@^10.0.0: + version "10.0.6" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5" + integrity sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ== + dependencies: + minipass "^7.0.3" + +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -717,14 +578,8 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -764,24 +619,19 @@ unique-slug@^4.0.0: dependencies: imurmurhash "^0.1.4" -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -which@^2.0.1, which@^2.0.2: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -wide-align@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== +which@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/which/-/which-4.0.0.tgz#cd60b5e74503a3fbcfbf6cd6b4138a8bae644c1a" + integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg== dependencies: - string-width "^1.0.2 || 2 || 3 || 4" + isexe "^3.1.1" "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" @@ -801,11 +651,6 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" diff --git a/build/package.json b/build/package.json index 2b89bbc1c99..0bbeed3f136 100644 --- a/build/package.json +++ b/build/package.json @@ -4,7 +4,7 @@ "license": "MIT", "devDependencies": { "@azure/cosmos": "^3", - "@azure/identity": "^3.4.1", + "@azure/identity": "^4.2.1", "@azure/storage-blob": "^12.17.0", "@electron/get": "^2.0.0", "@types/ansi-colors": "^3.2.0", @@ -44,7 +44,6 @@ "esbuild": "0.20.0", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", - "gulp-shell": "^0.8.0", "jsonc-parser": "^2.3.0", "mime": "^1.4.1", "mkdirp": "^1.0.4", diff --git a/build/yarn.lock b/build/yarn.lock index 3131c43217c..d99ceffaadf 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -9,20 +9,19 @@ dependencies: tslib "^2.0.0" +"@azure/abort-controller@^2.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz#42fe0ccab23841d9905812c58f1082d27784566d" + integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA== + dependencies: + tslib "^2.6.2" + "@azure/core-asynciterator-polyfill@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz#dcccebb88406e5c76e0e1d52e8cc4c43a68b3ee7" integrity sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg== -"@azure/core-auth@^1.3.0": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.3.2.tgz#6a2c248576c26df365f6c7881ca04b7f6d08e3d0" - integrity sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA== - dependencies: - "@azure/abort-controller" "^1.0.0" - tslib "^2.2.0" - -"@azure/core-auth@^1.5.0": +"@azure/core-auth@^1.3.0", "@azure/core-auth@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.5.0.tgz#a41848c5c31cb3b7c84c409885267d55a2c92e44" integrity sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw== @@ -81,22 +80,7 @@ dependencies: "@azure/core-asynciterator-polyfill" "^1.0.0" -"@azure/core-rest-pipeline@^1.1.0", "@azure/core-rest-pipeline@^1.2.0": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.3.2.tgz#82bfb4e960b4ecf4f1a1cdb1afde4ce9192aef09" - integrity sha512-kymICKESeHBpVLgQiAxllgWdSTopkqtmfPac8ITwMCxNEC6hzbSpqApYbjzxbBNkBMgoD4GESo6LLhR/sPh6kA== - dependencies: - "@azure/abort-controller" "^1.0.0" - "@azure/core-auth" "^1.3.0" - "@azure/core-tracing" "1.0.0-preview.13" - "@azure/logger" "^1.0.0" - form-data "^4.0.0" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - tslib "^2.2.0" - uuid "^8.3.0" - -"@azure/core-rest-pipeline@^1.5.0": +"@azure/core-rest-pipeline@^1.1.0", "@azure/core-rest-pipeline@^1.2.0", "@azure/core-rest-pipeline@^1.5.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.7.0.tgz#71f42c19af160422cc84513809ff9668d8047087" integrity sha512-e2awPzwMKHrmvYgZ0qIKNkqnCM1QoDs7A0rOiS3OSAlOQOz/kL7PPKHXwFMuBeaRvS8i7fgobJn79q2Cji5f+Q== @@ -126,21 +110,13 @@ dependencies: tslib "^2.2.0" -"@azure/core-util@^1.1.0", "@azure/core-util@^1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.6.1.tgz#fea221c4fa43c26543bccf799beb30c1c7878f5a" - integrity sha512-h5taHeySlsV9qxuK64KZxy4iln1BtMYlNt5jbuEFN3UFSAd1EwKg/Gjl5a6tZ/W8t6li3xPnutOx7zbDyXnPmQ== +"@azure/core-util@^1.1.0", "@azure/core-util@^1.1.1", "@azure/core-util@^1.3.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.9.0.tgz#469afd7e6452d5388b189f90d33f7756b0b210d1" + integrity sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw== dependencies: - "@azure/abort-controller" "^1.0.0" - tslib "^2.2.0" - -"@azure/core-util@^1.1.1": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.2.0.tgz#3499deba1fc36dda6f1912b791809b6f15d4a392" - integrity sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng== - dependencies: - "@azure/abort-controller" "^1.0.0" - tslib "^2.2.0" + "@azure/abort-controller" "^2.0.0" + tslib "^2.6.2" "@azure/cosmos@^3": version "3.17.3" @@ -161,20 +137,20 @@ universal-user-agent "^6.0.0" uuid "^8.3.0" -"@azure/identity@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-3.4.1.tgz#18ba48b7421c818ef8116e8eec3c03ec1a62649a" - integrity sha512-oQ/r5MBdfZTMIUcY5Ch8G7Vv9aIXDkEYyU4Dfqjim4MQN+LY2uiQ57P1JDopMLeHCsZxM4yy8lEdne3tM9Xhzg== +"@azure/identity@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.2.1.tgz#22b366201e989b7b41c0e1690e103bd579c31e4c" + integrity sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q== dependencies: "@azure/abort-controller" "^1.0.0" "@azure/core-auth" "^1.5.0" "@azure/core-client" "^1.4.0" "@azure/core-rest-pipeline" "^1.1.0" "@azure/core-tracing" "^1.0.0" - "@azure/core-util" "^1.6.1" + "@azure/core-util" "^1.3.0" "@azure/logger" "^1.0.0" - "@azure/msal-browser" "^3.5.0" - "@azure/msal-node" "^2.5.1" + "@azure/msal-browser" "^3.11.1" + "@azure/msal-node" "^2.9.2" events "^3.0.0" jws "^4.0.0" open "^8.0.0" @@ -188,24 +164,24 @@ dependencies: tslib "^2.0.0" -"@azure/msal-browser@^3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.5.0.tgz#eb64c931c78c2b75c70807f618e1284bbb183380" - integrity sha512-2NtMuel4CI3UEelCPKkNRXgKzpWEX48fvxIvPz7s0/sTcCaI08r05IOkH2GkXW+czUOtuY6+oGafJCpumnjRLg== +"@azure/msal-browser@^3.11.1": + version "3.17.0" + resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.17.0.tgz#dee9ccae586239e7e0708b261f7ffa5bc7e00fb7" + integrity sha512-csccKXmW2z7EkZ0I3yAoW/offQt+JECdTIV/KrnRoZyM7wCSsQWODpwod8ZhYy7iOyamcHApR9uCh0oD1M+0/A== dependencies: - "@azure/msal-common" "14.4.0" + "@azure/msal-common" "14.12.0" -"@azure/msal-common@14.4.0": - version "14.4.0" - resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.4.0.tgz#f938c1d96bb73d65baab985c96faaa273c97cfd5" - integrity sha512-ffCymScQuMKVj+YVfwNI52A5Tu+uiZO2eTf+c+3TXxdAssks4nokJhtr+uOOMxH0zDi6d1OjFKFKeXODK0YLSg== +"@azure/msal-common@14.12.0": + version "14.12.0" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.12.0.tgz#844abe269b071f8fa8949dadc2a7b65bbb147588" + integrity sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw== -"@azure/msal-node@^2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.5.1.tgz#d180a1ba5fdc611a318a8f018a2db3453e2e2898" - integrity sha512-PsPRISqCG253HQk1cAS7eJW7NWTbnBGpG+vcGGz5z4JYRdnM2EIXlj1aBpXCdozenEPtXEVvHn2ELleW1w82nQ== +"@azure/msal-node@^2.9.2": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.9.2.tgz#e6d3c1661012c1bd0ef68e328f73a2fdede52931" + integrity sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ== dependencies: - "@azure/msal-common" "14.4.0" + "@azure/msal-common" "14.12.0" jsonwebtoken "^9.0.0" uuid "^8.3.0" @@ -743,13 +719,6 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - ansi-wrap@0.1.0, ansi-wrap@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" @@ -879,11 +848,11 @@ brace-expansion@^1.1.7: concat-map "0.0.1" braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" buffer-alloc-unsafe@^1.1.0: version "1.1.0" @@ -966,14 +935,6 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - cheerio-select@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" @@ -1077,23 +1038,11 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" @@ -1422,10 +1371,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -1617,28 +1566,11 @@ gulp-merge-json@^2.1.1: through "^2.3.8" vinyl "^2.1.0" -gulp-shell@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/gulp-shell/-/gulp-shell-0.8.0.tgz#0ed4980de1d0c67e5f6cce971d7201fd0be50555" - integrity sha512-wHNCgmqbWkk1c6Gc2dOL5SprcoeujQdeepICwfQRo91DIylTE7a794VEE+leq3cE2YDoiS5ulvRfKVIEMazcTQ== - dependencies: - chalk "^3.0.0" - fancy-log "^1.3.3" - lodash.template "^4.5.0" - plugin-error "^1.0.1" - through2 "^3.0.1" - tslib "^1.10.0" - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -1917,31 +1849,11 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= - lodash.mergewith@^4.6.1: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== -lodash.template@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" - integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.templatesettings "^4.0.0" - -lodash.templatesettings@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" - integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -2559,13 +2471,6 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -2657,20 +2562,10 @@ tree-sitter@^0.20.5, tree-sitter@^0.20.6: nan "^2.18.0" prebuild-install "^7.1.1" -tslib@^1.10.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" - integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== - -tslib@^2.2.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.0.0, tslib@^2.2.0, tslib@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== tunnel-agent@^0.6.0: version "0.6.0" diff --git a/cglicenses.json b/cglicenses.json index 7643d1e4194..d90d55d7315 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -130,6 +130,21 @@ "SOFTWARE" ] }, + { + // Reason: NPM package does not include repository URL https://github.com/microsoft/vscode-deviceid/issues/12 + "name": "@vscode/deviceid", + "fullLicenseText": [ + "Copyright (c) Microsoft Corporation.", + "", + "MIT License", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + ] + }, { // Reason: Missing license file "name": "@tokenizer/token", @@ -317,13 +332,21 @@ "fullLicenseTextUri": "https://raw.githubusercontent.com/rodrimati1992/const_format_crates/b2207af46bfbd9f1a6bd12dbffd10feeea3d9fd7/LICENSE-ZLIB.md" }, { // License is MIT/Apache and tool doesn't look in subfolders - "name": "toml", - "fullLicenseTextUri": "https://raw.githubusercontent.com/toml-rs/toml/main/crates/toml/LICENSE-MIT" + "name": "toml_edit", + "fullLicenseTextUri": "https://raw.githubusercontent.com/toml-rs/toml/main/crates/toml_edit/LICENSE-MIT" + }, + { // License is MIT/Apache and tool doesn't look in subfolders + "name": "toml_datetime", + "fullLicenseTextUri": "https://raw.githubusercontent.com/toml-rs/toml/main/crates/toml_datetime/LICENSE-MIT" }, { // License is MIT/Apache and tool doesn't look in subfolders "name": "dirs-sys-next", "fullLicenseTextUri": "https://raw.githubusercontent.com/xdg-rs/dirs/master/dirs-sys/LICENSE-MIT" }, + { // License is MIT/Apache and gitlab API doesn't find the project + "name": "libredox", + "fullLicenseTextUri": "https://gitlab.redox-os.org/redox-os/libredox/-/raw/master/LICENSE" + }, { "name": "https-proxy-agent", "fullLicenseText": [ diff --git a/cgmanifest.json b/cgmanifest.json index f1e4192dc28..61747342eef 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "22f383dcd529d6bf790856db614a35fea78e825f" + "commitHash": "9b1bf44ea9e7785e38c93b7d22d32dbca262df6c" } }, "isOnlyProductionDependency": true, - "version": "20.9.0" + "version": "20.11.1" }, { "component": { diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index 1b98ce5b18c..a9630c12022 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -17,6 +17,38 @@ required to debug changes to any libraries licensed under the GNU Lesser General +--------------------------------------------------------- + +addr2line 0.21.0 - Apache-2.0 OR MIT +https://github.com/gimli-rs/addr2line + +Copyright (c) 2016-2018 The gimli Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + --------------------------------------------------------- adler 1.0.2 - 0BSD OR MIT OR Apache-2.0 @@ -49,7 +81,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -aho-corasick 1.0.1 - Unlicense OR MIT +aho-corasick 1.1.3 - Unlicense OR MIT https://github.com/BurntSushi/aho-corasick The MIT License (MIT) @@ -132,7 +164,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -anstream 0.3.2 - MIT OR Apache-2.0 +anstream 0.6.14 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -157,7 +189,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -anstyle 1.0.0 - MIT OR Apache-2.0 +anstyle 1.0.7 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -182,7 +214,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -anstyle-parse 0.2.0 - MIT OR Apache-2.0 +anstyle-parse 0.2.4 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -207,7 +239,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -anstyle-query 1.0.0 - MIT OR Apache-2.0 +anstyle-query 1.0.3 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -232,7 +264,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -anstyle-wincon 1.0.1 - MIT OR Apache-2.0 +anstyle-wincon 3.0.3 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -285,7 +317,7 @@ SOFTWARE. --------------------------------------------------------- -async-channel 1.8.0 - Apache-2.0 OR MIT +async-channel 2.3.1 - Apache-2.0 OR MIT https://github.com/smol-rs/async-channel Permission is hereby granted, free of charge, to any @@ -315,7 +347,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-io 1.9.0 - Apache-2.0 OR MIT +async-io 1.13.0 - Apache-2.0 OR MIT +async-io 2.3.2 - Apache-2.0 OR MIT https://github.com/smol-rs/async-io Permission is hereby granted, free of charge, to any @@ -345,7 +378,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-lock 2.7.0 - Apache-2.0 OR MIT +async-lock 2.8.0 - Apache-2.0 OR MIT +async-lock 3.3.0 - Apache-2.0 OR MIT https://github.com/smol-rs/async-lock Permission is hereby granted, free of charge, to any @@ -375,7 +409,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-process 1.7.0 - Apache-2.0 OR MIT +async-process 1.8.1 - Apache-2.0 OR MIT https://github.com/smol-rs/async-process Permission is hereby granted, free of charge, to any @@ -405,7 +439,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-recursion 1.0.0 - MIT OR Apache-2.0 +async-recursion 1.1.1 - MIT OR Apache-2.0 https://github.com/dcchut/async-recursion Permission is hereby granted, free of charge, to any @@ -435,7 +469,37 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-task 4.4.0 - Apache-2.0 OR MIT +async-signal 0.2.6 - Apache-2.0 OR MIT +https://github.com/smol-rs/async-signal + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +async-task 4.7.1 - Apache-2.0 OR MIT https://github.com/smol-rs/async-task Permission is hereby granted, free of charge, to any @@ -465,7 +529,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -async-trait 0.1.68 - MIT OR Apache-2.0 +async-trait 0.1.80 - MIT OR Apache-2.0 https://github.com/dtolnay/async-trait Permission is hereby granted, free of charge, to any @@ -495,7 +559,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -atomic-waker 1.1.1 - Apache-2.0 OR MIT +atomic-waker 1.1.2 - Apache-2.0 OR MIT https://github.com/smol-rs/atomic-waker Permission is hereby granted, free of charge, to any @@ -525,7 +589,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -autocfg 1.1.0 - Apache-2.0 OR MIT +autocfg 1.3.0 - Apache-2.0 OR MIT https://github.com/cuviper/autocfg Copyright (c) 2018 Josh Stone @@ -557,7 +621,39 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -base64 0.21.2 - MIT OR Apache-2.0 +backtrace 0.3.71 - MIT OR Apache-2.0 +https://github.com/rust-lang/backtrace-rs + +Copyright (c) 2014 Alex Crichton + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +base64 0.21.7 - MIT OR Apache-2.0 https://github.com/marshallpierce/rust-base64 The MIT License (MIT) @@ -618,7 +714,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- bitflags 1.3.2 - MIT/Apache-2.0 -bitflags 2.4.1 - MIT OR Apache-2.0 +bitflags 2.5.0 - MIT OR Apache-2.0 https://github.com/bitflags/bitflags Copyright (c) 2014 The Rust Project Developers @@ -650,7 +746,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -block-buffer 0.10.3 - MIT OR Apache-2.0 +block-buffer 0.10.4 - MIT OR Apache-2.0 https://github.com/RustCrypto/utils All crates licensed under either of @@ -704,7 +800,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -block-padding 0.3.2 - MIT OR Apache-2.0 +block-padding 0.3.3 - MIT OR Apache-2.0 https://github.com/RustCrypto/utils All crates licensed under either of @@ -758,7 +854,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -blocking 1.3.1 - Apache-2.0 OR MIT +blocking 1.6.0 - Apache-2.0 OR MIT https://github.com/smol-rs/blocking Permission is hereby granted, free of charge, to any @@ -788,7 +884,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -bumpalo 3.12.0 - MIT/Apache-2.0 +bumpalo 3.16.0 - MIT OR Apache-2.0 https://github.com/fitzgen/bumpalo Copyright (c) 2019 Nick Fitzgerald @@ -820,7 +916,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -byteorder 1.4.3 - Unlicense OR MIT +byteorder 1.5.0 - Unlicense OR MIT https://github.com/BurntSushi/byteorder The MIT License (MIT) @@ -848,7 +944,7 @@ THE SOFTWARE. --------------------------------------------------------- -bytes 1.4.0 - MIT +bytes 1.6.0 - MIT https://github.com/tokio-rs/bytes The MIT License (MIT) @@ -882,37 +978,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -cache-padded 1.2.0 - Apache-2.0 OR MIT -https://github.com/smol-rs/cache-padded - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -cc 1.0.73 - MIT/Apache-2.0 +cc 1.0.98 - MIT OR Apache-2.0 https://github.com/rust-lang/cc-rs Copyright (c) 2014 Alex Crichton @@ -976,7 +1042,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -chrono 0.4.26 - MIT/Apache-2.0 +chrono 0.4.38 - MIT OR Apache-2.0 https://github.com/chronotope/chrono Rust-chrono is dual-licensed under The MIT License [1] and @@ -1222,7 +1288,7 @@ limitations under the License. --------------------------------------------------------- -clap 4.3.0 - MIT OR Apache-2.0 +clap 4.5.4 - MIT OR Apache-2.0 https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -1248,7 +1314,7 @@ SOFTWARE. --------------------------------------------------------- -clap_builder 4.3.0 - MIT OR Apache-2.0 +clap_builder 4.5.2 - MIT OR Apache-2.0 https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -1274,8 +1340,8 @@ SOFTWARE. --------------------------------------------------------- -clap_derive 4.3.0 - MIT OR Apache-2.0 -https://github.com/clap-rs/clap/tree/master/clap_derive +clap_derive 4.5.4 - MIT OR Apache-2.0 +https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -1300,8 +1366,8 @@ SOFTWARE. --------------------------------------------------------- -clap_lex 0.5.0 - MIT OR Apache-2.0 -https://github.com/clap-rs/clap/tree/master/clap_lex +clap_lex 0.7.0 - MIT OR Apache-2.0 +https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -1326,215 +1392,7 @@ SOFTWARE. --------------------------------------------------------- -codespan-reporting 0.11.1 - Apache-2.0 -https://github.com/brendanzab/codespan - -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---------------------------------------------------------- - ---------------------------------------------------------- - -colorchoice 1.0.0 - MIT OR Apache-2.0 +colorchoice 1.0.1 - MIT OR Apache-2.0 https://github.com/rust-cli/anstyle This software is released under the MIT license: @@ -1559,8 +1417,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -concurrent-queue 1.2.4 - Apache-2.0 OR MIT -concurrent-queue 2.2.0 - Apache-2.0 OR MIT +concurrent-queue 2.5.0 - Apache-2.0 OR MIT https://github.com/smol-rs/concurrent-queue Permission is hereby granted, free of charge, to any @@ -1590,7 +1447,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -console 0.15.7 - MIT +console 0.15.8 - MIT https://github.com/console-rs/console The MIT License (MIT) @@ -1618,7 +1475,7 @@ SOFTWARE. --------------------------------------------------------- -const_format 0.2.31 - Zlib +const_format 0.2.32 - Zlib https://github.com/rodrimati1992/const_format_crates/ Copyright (c) 2020 Matias Rodriguez. @@ -1642,7 +1499,7 @@ freely, subject to the following restrictions: --------------------------------------------------------- -const_format_proc_macros 0.2.31 - Zlib +const_format_proc_macros 0.2.32 - Zlib https://github.com/rodrimati1992/const_format_crates/ Copyright (c) 2020 Matias Rodriguez. @@ -1666,7 +1523,7 @@ freely, subject to the following restrictions: --------------------------------------------------------- -core-foundation 0.9.3 - MIT / Apache-2.0 +core-foundation 0.9.4 - MIT OR Apache-2.0 https://github.com/servo/core-foundation-rs Copyright (c) 2012-2013 Mozilla Foundation @@ -1698,7 +1555,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -core-foundation-sys 0.8.3 - MIT / Apache-2.0 +core-foundation-sys 0.8.6 - MIT OR Apache-2.0 https://github.com/servo/core-foundation-rs Copyright (c) 2012-2013 Mozilla Foundation @@ -1730,7 +1587,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -cpufeatures 0.2.8 - MIT OR Apache-2.0 +cpufeatures 0.2.12 - MIT OR Apache-2.0 https://github.com/RustCrypto/utils All crates licensed under either of @@ -1784,7 +1641,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -crc32fast 1.3.2 - MIT OR Apache-2.0 +crc32fast 1.4.1 - MIT OR Apache-2.0 https://github.com/srijs/rust-crc32fast MIT License @@ -1812,7 +1669,7 @@ SOFTWARE. --------------------------------------------------------- -crossbeam-channel 0.5.12 - MIT OR Apache-2.0 +crossbeam-channel 0.5.13 - MIT OR Apache-2.0 https://github.com/crossbeam-rs/crossbeam The MIT License (MIT) @@ -1846,7 +1703,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -crossbeam-utils 0.8.19 - MIT OR Apache-2.0 +crossbeam-utils 0.8.20 - MIT OR Apache-2.0 https://github.com/crossbeam-rs/crossbeam The MIT License (MIT) @@ -1936,127 +1793,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -cxx 1.0.97 - MIT OR Apache-2.0 -https://github.com/dtolnay/cxx - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -cxx-build 1.0.97 - MIT OR Apache-2.0 -https://github.com/dtolnay/cxx - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -cxxbridge-flags 1.0.97 - MIT OR Apache-2.0 -https://github.com/dtolnay/cxx - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -cxxbridge-macro 1.0.97 - MIT OR Apache-2.0 -https://github.com/dtolnay/cxx - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -data-encoding 2.3.2 - MIT +data-encoding 2.6.0 - MIT https://github.com/ia0/data-encoding The MIT License (MIT) @@ -2085,6 +1822,32 @@ SOFTWARE. --------------------------------------------------------- +deranged 0.3.11 - MIT OR Apache-2.0 +https://github.com/jhpratt/deranged + +Copyright (c) 2022 Jacob Pratt et al. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + derivative 2.2.0 - MIT/Apache-2.0 https://github.com/mcarton/rust-derivative @@ -2139,7 +1902,7 @@ SOFTWARE. --------------------------------------------------------- -digest 0.10.5 - MIT OR Apache-2.0 +digest 0.10.7 - MIT OR Apache-2.0 https://github.com/RustCrypto/traits All crates licensed under either of @@ -2273,7 +2036,7 @@ SOFTWARE --------------------------------------------------------- -encoding_rs 0.8.31 - (Apache-2.0 OR MIT) AND BSD-3-Clause +encoding_rs 0.8.34 - (Apache-2.0 OR MIT) AND BSD-3-Clause https://github.com/hsivonen/encoding_rs Copyright Mozilla Foundation @@ -2305,7 +2068,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -enumflags2 0.7.7 - MIT OR Apache-2.0 +enumflags2 0.7.9 - MIT OR Apache-2.0 https://github.com/meithecatte/enumflags2 Copyright (c) 2017-2023 Maik Klein, Maja KÄ…dzioÅ‚ka @@ -2337,7 +2100,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -enumflags2_derive 0.7.7 - MIT OR Apache-2.0 +enumflags2_derive 0.7.9 - MIT OR Apache-2.0 https://github.com/meithecatte/enumflags2 Copyright (c) 2017-2023 Maik Klein, Maja KÄ…dzioÅ‚ka @@ -2401,7 +2164,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -errno 0.3.1 - MIT OR Apache-2.0 +errno 0.3.9 - MIT OR Apache-2.0 https://github.com/lambda-fairy/rust-errno Copyright (c) 2014 Chris Wong @@ -2433,35 +2196,10 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -errno-dragonfly 0.1.2 - MIT -https://github.com/mneumann/errno-dragonfly-rs - -MIT License - -Copyright (c) 2017 Michael Neumann - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - event-listener 2.5.3 - Apache-2.0 OR MIT +event-listener 3.1.0 - Apache-2.0 OR MIT +event-listener 4.0.3 - Apache-2.0 OR MIT +event-listener 5.3.0 - Apache-2.0 OR MIT https://github.com/smol-rs/event-listener Permission is hereby granted, free of charge, to any @@ -2491,7 +2229,39 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -fastrand 1.8.0 - Apache-2.0 OR MIT +event-listener-strategy 0.4.0 - Apache-2.0 OR MIT +event-listener-strategy 0.5.2 - Apache-2.0 OR MIT +https://github.com/smol-rs/event-listener-strategy + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +fastrand 1.9.0 - Apache-2.0 OR MIT +fastrand 2.1.0 - Apache-2.0 OR MIT https://github.com/smol-rs/fastrand Permission is hereby granted, free of charge, to any @@ -2521,7 +2291,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -filetime 0.2.17 - MIT/Apache-2.0 +filetime 0.2.23 - MIT/Apache-2.0 https://github.com/alexcrichton/filetime Copyright (c) 2014 Alex Crichton @@ -2553,7 +2323,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -flate2 1.0.26 - MIT OR Apache-2.0 +flate2 1.0.30 - MIT OR Apache-2.0 https://github.com/rust-lang/flate2-rs Copyright (c) 2014 Alex Crichton @@ -2669,7 +2439,7 @@ SOFTWARE. --------------------------------------------------------- -form_urlencoded 1.1.0 - MIT OR Apache-2.0 +form_urlencoded 1.2.1 - MIT OR Apache-2.0 https://github.com/servo/rust-url Copyright (c) 2013-2022 The rust-url developers @@ -2701,7 +2471,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures 0.3.28 - MIT OR Apache-2.0 +futures 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2734,7 +2504,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-channel 0.3.28 - MIT OR Apache-2.0 +futures-channel 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2767,7 +2537,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-core 0.3.28 - MIT OR Apache-2.0 +futures-core 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2800,7 +2570,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-executor 0.3.28 - MIT OR Apache-2.0 +futures-executor 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2833,7 +2603,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-io 0.3.28 - MIT OR Apache-2.0 +futures-io 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2866,7 +2636,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-lite 1.12.0 - Apache-2.0 OR MIT +futures-lite 1.13.0 - Apache-2.0 OR MIT +futures-lite 2.3.0 - Apache-2.0 OR MIT https://github.com/smol-rs/futures-lite Permission is hereby granted, free of charge, to any @@ -2896,7 +2667,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-macro 0.3.28 - MIT OR Apache-2.0 +futures-macro 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2929,7 +2700,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-sink 0.3.28 - MIT OR Apache-2.0 +futures-sink 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2962,7 +2733,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-task 0.3.28 - MIT OR Apache-2.0 +futures-task 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -2995,7 +2766,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -futures-util 0.3.28 - MIT OR Apache-2.0 +futures-util 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/futures-rs Copyright (c) 2016 Alex Crichton @@ -3028,7 +2799,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -generic-array 0.14.6 - MIT +generic-array 0.14.7 - MIT https://github.com/fizyk20/generic-array The MIT License (MIT) @@ -3265,7 +3036,7 @@ limitations under the License. --------------------------------------------------------- getrandom 0.1.16 - MIT OR Apache-2.0 -getrandom 0.2.7 - MIT OR Apache-2.0 +getrandom 0.2.15 - MIT OR Apache-2.0 https://github.com/rust-random/getrandom Copyright (c) 2018-2024 The rust-random Project Developers @@ -3298,6 +3069,38 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- +gimli 0.28.1 - MIT OR Apache-2.0 +https://github.com/gimli-rs/gimli + +Copyright (c) 2015 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + h2 0.3.26 - MIT https://github.com/hyperium/h2 @@ -3333,7 +3136,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- hashbrown 0.12.3 - MIT OR Apache-2.0 -hashbrown 0.14.3 - MIT OR Apache-2.0 +hashbrown 0.14.5 - MIT OR Apache-2.0 https://github.com/rust-lang/hashbrown Copyright (c) 2016 Amanieu d'Antras @@ -3365,7 +3168,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -heck 0.4.0 - MIT OR Apache-2.0 +heck 0.5.0 - MIT OR Apache-2.0 https://github.com/withoutboats/heck Copyright (c) 2015 The Rust Project Developers @@ -3397,8 +3200,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -hermit-abi 0.1.19 - MIT/Apache-2.0 -hermit-abi 0.3.1 - MIT OR Apache-2.0 +hermit-abi 0.3.9 - MIT OR Apache-2.0 https://github.com/hermit-os/hermit-rs Permission is hereby granted, free of charge, to any @@ -3557,7 +3359,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -http 0.2.9 - MIT OR Apache-2.0 +http 0.2.12 - MIT OR Apache-2.0 https://github.com/hyperium/http Copyright (c) 2017 http-rs authors @@ -3589,7 +3391,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -http-body 0.4.5 - MIT +http-body 0.4.6 - MIT https://github.com/hyperium/http-body The MIT License (MIT) @@ -3626,7 +3428,7 @@ DEALINGS IN THE SOFTWARE. httparse 1.8.0 - MIT/Apache-2.0 https://github.com/seanmonstar/httparse -Copyright (c) 2015-2021 Sean McArthur +Copyright (c) 2015-2024 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -3649,7 +3451,7 @@ THE SOFTWARE. --------------------------------------------------------- -httpdate 1.0.2 - MIT/Apache-2.0 +httpdate 1.0.3 - MIT OR Apache-2.0 https://github.com/pyfisch/httpdate Copyright (c) 2016 Pyfisch @@ -3675,7 +3477,7 @@ THE SOFTWARE. --------------------------------------------------------- -hyper 0.14.26 - MIT +hyper 0.14.28 - MIT https://github.com/hyperium/hyper The MIT License (MIT) @@ -3729,7 +3531,7 @@ THE SOFTWARE. --------------------------------------------------------- -iana-time-zone 0.1.57 - MIT OR Apache-2.0 +iana-time-zone 0.1.60 - MIT OR Apache-2.0 https://github.com/strawlab/iana-time-zone Copyright (c) 2020 Andrew D. Straw @@ -3761,7 +3563,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -iana-time-zone-haiku 0.1.1 - MIT OR Apache-2.0 +iana-time-zone-haiku 0.1.2 - MIT OR Apache-2.0 https://github.com/strawlab/iana-time-zone Copyright (c) 2020 Andrew D. Straw @@ -3793,7 +3595,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -idna 0.3.0 - MIT OR Apache-2.0 +idna 0.5.0 - MIT OR Apache-2.0 https://github.com/servo/rust-url/ Copyright (c) 2013-2022 The rust-url developers @@ -3826,7 +3628,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- indexmap 1.9.3 - Apache-2.0 OR MIT -indexmap 2.1.0 - Apache-2.0 OR MIT +indexmap 2.2.6 - Apache-2.0 OR MIT https://github.com/indexmap-rs/indexmap Copyright (c) 2016--2017 @@ -3858,7 +3660,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -indicatif 0.17.4 - MIT +indicatif 0.17.8 - MIT https://github.com/console-rs/indicatif The MIT License (MIT) @@ -3940,7 +3742,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -instant 0.1.12 - BSD-3-Clause +instant 0.1.13 - BSD-3-Clause https://github.com/sebcrozet/instant Copyright (c) 2019, Sébastien Crozet @@ -4004,7 +3806,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -ipnet 2.5.0 - MIT OR Apache-2.0 +ipnet 2.9.0 - MIT OR Apache-2.0 https://github.com/krisprice/ipnet Copyright 2017 Juniper Networks, Inc. @@ -4046,38 +3848,6 @@ SOFTWARE. --------------------------------------------------------- -is-terminal 0.4.7 - MIT -https://github.com/sunfishcode/is-terminal - -The MIT License (MIT) - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - is-wsl 0.4.0 - MIT https://github.com/TheLarkInn/is-wsl @@ -4106,7 +3876,33 @@ SOFTWARE. --------------------------------------------------------- -itoa 1.0.4 - MIT OR Apache-2.0 +is_terminal_polyfill 1.70.0 - MIT OR Apache-2.0 +https://github.com/polyfill-rs/is_terminal_polyfill + +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +itoa 1.0.11 - MIT OR Apache-2.0 https://github.com/dtolnay/itoa Permission is hereby granted, free of charge, to any @@ -4136,7 +3932,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -js-sys 0.3.60 - MIT/Apache-2.0 +js-sys 0.3.69 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/js-sys Copyright (c) 2014 Alex Crichton @@ -4168,7 +3964,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -keyring 2.0.3 - MIT OR Apache-2.0 +keyring 2.3.3 - MIT OR Apache-2.0 https://github.com/hwchen/keyring-rs Copyright (c) 2016 keyring Developers @@ -4232,7 +4028,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -libc 0.2.153 - MIT OR Apache-2.0 +libc 0.2.155 - MIT OR Apache-2.0 https://github.com/rust-lang/libc Copyright (c) 2014-2020 The Rust Project Developers @@ -4264,7 +4060,35 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -libz-sys 1.1.12 - MIT OR Apache-2.0 +libredox 0.1.3 - MIT +https://gitlab.redox-os.org/redox-os/libredox.git + +MIT License + +Copyright (c) 2023 4lDO2 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +libz-sys 1.1.16 - MIT OR Apache-2.0 https://github.com/rust-lang/libz-sys Copyright (c) 2014 Alex Crichton @@ -4297,37 +4121,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -link-cplusplus 1.0.9 - MIT OR Apache-2.0 -https://github.com/dtolnay/link-cplusplus - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -linux-keyutils 0.2.3 - Apache-2.0 OR MIT +linux-keyutils 0.2.4 - Apache-2.0 OR MIT https://github.com/landhb/linux-keyutils Licensed under either of the following at your discretion: @@ -4359,6 +4153,7 @@ additional terms or conditions. --------------------------------------------------------- linux-raw-sys 0.3.8 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT +linux-raw-sys 0.4.14 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT https://github.com/sunfishcode/linux-raw-sys Permission is hereby granted, free of charge, to any @@ -4388,7 +4183,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -lock_api 0.4.9 - MIT OR Apache-2.0 +lock_api 0.4.12 - MIT OR Apache-2.0 https://github.com/Amanieu/parking_lot Copyright (c) 2016 The Rust Project Developers @@ -4420,7 +4215,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -log 0.4.18 - MIT OR Apache-2.0 +log 0.4.21 - MIT OR Apache-2.0 https://github.com/rust-lang/log Copyright (c) 2014 The Rust Project Developers @@ -4506,7 +4301,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -memchr 2.5.0 - Unlicense/MIT +memchr 2.7.2 - Unlicense OR MIT https://github.com/BurntSushi/memchr The MIT License (MIT) @@ -4535,6 +4330,7 @@ THE SOFTWARE. --------------------------------------------------------- memoffset 0.7.1 - MIT +memoffset 0.9.1 - MIT https://github.com/Gilnaa/memoffset The MIT License (MIT) @@ -4562,7 +4358,7 @@ SOFTWARE. --------------------------------------------------------- -mime 0.3.16 - MIT/Apache-2.0 +mime 0.3.17 - MIT OR Apache-2.0 https://github.com/hyperium/mime Copyright (c) 2014-2019 Sean McArthur @@ -4588,7 +4384,7 @@ THE SOFTWARE. --------------------------------------------------------- -miniz_oxide 0.7.1 - MIT OR Zlib OR Apache-2.0 +miniz_oxide 0.7.3 - MIT OR Zlib OR Apache-2.0 https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide This library (excluding the miniz C code used for tests) is licensed under the MIT license. The library is based on the miniz C library, of which the parts used are dual-licensed under the [MIT license](https://github.com/Frommi/miniz_oxide/blob/master/miniz/miniz.c#L1) and also the [unlicense](https://github.com/Frommi/miniz_oxide/blob/master/miniz/miniz.c#L577). @@ -4651,7 +4447,7 @@ SOFTWARE. --------------------------------------------------------- -nix 0.26.2 - MIT +nix 0.26.4 - MIT https://github.com/nix-rust/nix The MIT License (MIT) @@ -4679,7 +4475,7 @@ THE SOFTWARE. --------------------------------------------------------- -ntapi 0.4.0 - Apache-2.0 OR MIT +ntapi 0.4.1 - Apache-2.0 OR MIT https://github.com/MSxDOS/ntapi Permission is hereby granted, free of charge, to any person obtaining a copy @@ -4703,7 +4499,7 @@ SOFTWARE. --------------------------------------------------------- -num 0.4.0 - MIT OR Apache-2.0 +num 0.4.3 - MIT OR Apache-2.0 https://github.com/rust-num/num Copyright (c) 2014 The Rust Project Developers @@ -4735,7 +4531,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-bigint 0.4.3 - MIT OR Apache-2.0 +num-bigint 0.4.5 - MIT OR Apache-2.0 https://github.com/rust-num/num-bigint Copyright (c) 2014 The Rust Project Developers @@ -4767,7 +4563,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-complex 0.4.2 - MIT OR Apache-2.0 +num-complex 0.4.6 - MIT OR Apache-2.0 https://github.com/rust-num/num-complex Copyright (c) 2014 The Rust Project Developers @@ -4799,7 +4595,33 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-integer 0.1.45 - MIT OR Apache-2.0 +num-conv 0.1.0 - MIT OR Apache-2.0 +https://github.com/jhpratt/num-conv + +Copyright (c) 2023 Jacob Pratt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +num-integer 0.1.46 - MIT OR Apache-2.0 https://github.com/rust-num/num-integer Copyright (c) 2014 The Rust Project Developers @@ -4831,7 +4653,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-iter 0.1.43 - MIT OR Apache-2.0 +num-iter 0.1.45 - MIT OR Apache-2.0 https://github.com/rust-num/num-iter Copyright (c) 2014 The Rust Project Developers @@ -4863,7 +4685,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-rational 0.4.1 - MIT OR Apache-2.0 +num-rational 0.4.2 - MIT OR Apache-2.0 https://github.com/rust-num/num-rational Copyright (c) 2014 The Rust Project Developers @@ -4895,7 +4717,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num-traits 0.2.15 - MIT OR Apache-2.0 +num-traits 0.2.19 - MIT OR Apache-2.0 https://github.com/rust-num/num-traits Copyright (c) 2014 The Rust Project Developers @@ -4927,7 +4749,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -num_cpus 1.13.1 - MIT OR Apache-2.0 +num_cpus 1.16.0 - MIT OR Apache-2.0 https://github.com/seanmonstar/num_cpus Copyright (c) 2015 @@ -4981,7 +4803,39 @@ SOFTWARE. --------------------------------------------------------- -once_cell 1.17.2 - MIT OR Apache-2.0 +object 0.32.2 - Apache-2.0 OR MIT +https://github.com/gimli-rs/object + +Copyright (c) 2015 The Gimli Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +once_cell 1.19.0 - MIT OR Apache-2.0 https://github.com/matklad/once_cell Permission is hereby granted, free of charge, to any @@ -5011,7 +4865,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -open 4.1.0 - MIT +open 4.2.0 - MIT https://github.com/Byron/open-rs The MIT License (MIT) @@ -5042,7 +4896,7 @@ OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -openssl 0.10.60 - Apache-2.0 +openssl 0.10.64 - Apache-2.0 https://github.com/sfackler/rust-openssl Copyright 2011-2017 Google Inc. @@ -5064,7 +4918,7 @@ limitations under the License. --------------------------------------------------------- -openssl-macros 0.1.0 - MIT/Apache-2.0 +openssl-macros 0.1.1 - MIT/Apache-2.0 This software is released under the MIT license: @@ -5121,7 +4975,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -openssl-sys 0.9.96 - MIT +openssl-sys 0.9.102 - MIT https://github.com/sfackler/rust-openssl The MIT License (MIT) @@ -6189,7 +6043,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -os_info 3.7.0 - MIT +os_info 3.8.2 - MIT https://github.com/stanislav-tkach/os_info The MIT License (MIT) @@ -6217,7 +6071,7 @@ SOFTWARE. --------------------------------------------------------- -parking 2.0.0 - Apache-2.0 OR MIT +parking 2.2.0 - Apache-2.0 OR MIT https://github.com/smol-rs/parking Permission is hereby granted, free of charge, to any @@ -6247,7 +6101,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -parking_lot 0.12.1 - MIT OR Apache-2.0 +parking_lot 0.12.2 - MIT OR Apache-2.0 https://github.com/Amanieu/parking_lot Copyright (c) 2016 The Rust Project Developers @@ -6279,7 +6133,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -parking_lot_core 0.9.3 - MIT OR Apache-2.0 +parking_lot_core 0.9.10 - MIT OR Apache-2.0 https://github.com/Amanieu/parking_lot Copyright (c) 2016 The Rust Project Developers @@ -6311,7 +6165,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -paste 1.0.9 - MIT OR Apache-2.0 +paste 1.0.15 - MIT OR Apache-2.0 https://github.com/dtolnay/paste Permission is hereby granted, free of charge, to any @@ -6371,7 +6225,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -percent-encoding 2.2.0 - MIT OR Apache-2.0 +percent-encoding 2.3.1 - MIT OR Apache-2.0 https://github.com/servo/rust-url/ Copyright (c) 2013-2022 The rust-url developers @@ -6403,7 +6257,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -pin-project 1.1.0 - Apache-2.0 OR MIT +pin-project 1.1.5 - Apache-2.0 OR MIT https://github.com/taiki-e/pin-project Permission is hereby granted, free of charge, to any @@ -6433,7 +6287,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -pin-project-internal 1.1.0 - Apache-2.0 OR MIT +pin-project-internal 1.1.5 - Apache-2.0 OR MIT https://github.com/taiki-e/pin-project Permission is hereby granted, free of charge, to any @@ -6463,7 +6317,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -pin-project-lite 0.2.9 - Apache-2.0 OR MIT +pin-project-lite 0.2.14 - Apache-2.0 OR MIT https://github.com/taiki-e/pin-project-lite Permission is hereby granted, free of charge, to any @@ -6525,7 +6379,37 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -pkg-config 0.3.25 - MIT OR Apache-2.0 +piper 0.2.2 - MIT OR Apache-2.0 +https://github.com/smol-rs/piper + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +pkg-config 0.3.30 - MIT OR Apache-2.0 https://github.com/rust-lang/pkg-config-rs Copyright (c) 2014 Alex Crichton @@ -6557,7 +6441,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -polling 2.3.0 - Apache-2.0 OR MIT +polling 2.8.0 - Apache-2.0 OR MIT +polling 3.7.0 - Apache-2.0 OR MIT https://github.com/smol-rs/polling Permission is hereby granted, free of charge, to any @@ -6587,7 +6472,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -portable-atomic 1.3.3 - Apache-2.0 OR MIT +portable-atomic 1.6.0 - Apache-2.0 OR MIT https://github.com/taiki-e/portable-atomic Permission is hereby granted, free of charge, to any @@ -6617,7 +6502,33 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -ppv-lite86 0.2.16 - MIT/Apache-2.0 +powerfmt 0.2.0 - MIT OR Apache-2.0 +https://github.com/jhpratt/powerfmt + +Copyright (c) 2023 Jacob Pratt et al. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +ppv-lite86 0.2.17 - MIT/Apache-2.0 https://github.com/cryptocorrosion/cryptocorrosion Copyright (c) 2019 The CryptoCorrosion Contributors @@ -6649,7 +6560,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -proc-macro-crate 1.2.1 - Apache-2.0/MIT +proc-macro-crate 1.3.1 - MIT OR Apache-2.0 https://github.com/bkchr/proc-macro-crate Permission is hereby granted, free of charge, to any @@ -6679,7 +6590,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -proc-macro2 1.0.80 - MIT OR Apache-2.0 +proc-macro2 1.0.83 - MIT OR Apache-2.0 https://github.com/dtolnay/proc-macro2 Permission is hereby granted, free of charge, to any @@ -6709,7 +6620,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -quote 1.0.28 - MIT OR Apache-2.0 +quote 1.0.36 - MIT OR Apache-2.0 https://github.com/dtolnay/quote Permission is hereby granted, free of charge, to any @@ -6874,8 +6785,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -redox_syscall 0.2.16 - MIT -redox_syscall 0.3.5 - MIT +redox_syscall 0.4.1 - MIT +redox_syscall 0.5.1 - MIT https://github.com/redox-os/syscall Copyright (c) 2017 Redox OS Developers @@ -6904,7 +6815,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -redox_users 0.4.3 - MIT +redox_users 0.4.5 - MIT https://gitlab.redox-os.org/redox-os/users The MIT License (MIT) @@ -6932,7 +6843,7 @@ SOFTWARE. --------------------------------------------------------- -regex 1.8.3 - MIT OR Apache-2.0 +regex 1.10.4 - MIT OR Apache-2.0 https://github.com/rust-lang/regex Copyright (c) 2014 The Rust Project Developers @@ -6964,7 +6875,39 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -regex-syntax 0.7.2 - MIT OR Apache-2.0 +regex-automata 0.4.6 - MIT OR Apache-2.0 +https://github.com/rust-lang/regex/tree/master/regex-automata + +Copyright (c) 2014 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +regex-syntax 0.8.3 - MIT OR Apache-2.0 https://github.com/rust-lang/regex/tree/master/regex-syntax Copyright (c) 2014 The Rust Project Developers @@ -6996,7 +6939,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -reqwest 0.11.22 - MIT OR Apache-2.0 +reqwest 0.11.27 - MIT OR Apache-2.0 https://github.com/seanmonstar/reqwest Copyright (c) 2016 Sean McArthur @@ -7022,7 +6965,7 @@ THE SOFTWARE. --------------------------------------------------------- -rmp 0.8.11 - MIT +rmp 0.8.14 - MIT https://github.com/3Hren/msgpack-rust MIT License @@ -7050,7 +6993,7 @@ SOFTWARE. --------------------------------------------------------- -rmp-serde 1.1.1 - MIT +rmp-serde 1.3.0 - MIT https://github.com/3Hren/msgpack-rust MIT License @@ -7078,7 +7021,7 @@ SOFTWARE. --------------------------------------------------------- -russh 6a15199c784c0b6d171a6fec09ed730a5cd1350d +russh fd4f608a83753f9f3e137f95600faffede71cf65 https://github.com/microsoft/vscode-russh Apache License @@ -7286,7 +7229,7 @@ Apache License --------------------------------------------------------- -russh-cryptovec 6a15199c784c0b6d171a6fec09ed730a5cd1350d +russh-cryptovec fd4f608a83753f9f3e137f95600faffede71cf65 https://github.com/microsoft/vscode-russh Apache License @@ -7494,7 +7437,7 @@ Apache License --------------------------------------------------------- -russh-keys 6a15199c784c0b6d171a6fec09ed730a5cd1350d +russh-keys fd4f608a83753f9f3e137f95600faffede71cf65 https://github.com/microsoft/vscode-russh Apache License @@ -7702,7 +7645,40 @@ Apache License --------------------------------------------------------- -rustix 0.37.25 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT +rustc-demangle 0.1.24 - MIT/Apache-2.0 +https://github.com/rust-lang/rustc-demangle + +Copyright (c) 2014 Alex Crichton + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +rustix 0.37.27 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT +rustix 0.38.34 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT https://github.com/bytecodealliance/rustix Permission is hereby granted, free of charge, to any @@ -7732,7 +7708,23 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -ryu 1.0.11 - Apache-2.0 OR BSL-1.0 +rustls-pemfile 1.0.4 - Apache-2.0 OR ISC OR MIT +https://github.com/rustls/pemfile + +rustls-pemfile is distributed under the following three licenses: + +- Apache License version 2.0. +- MIT license. +- ISC license. + +These are included as LICENSE-APACHE, LICENSE-MIT and LICENSE-ISC +respectively. You may use this software under the terms of any +of these licenses, at your option. +--------------------------------------------------------- + +--------------------------------------------------------- + +ryu 1.0.18 - Apache-2.0 OR BSL-1.0 https://github.com/dtolnay/ryu @@ -7752,7 +7744,7 @@ be dual licensed as above, without any additional terms or conditions. --------------------------------------------------------- -schannel 0.1.20 - MIT +schannel 0.1.23 - MIT https://github.com/steffengy/schannel-rs The MIT License (MIT) @@ -7768,7 +7760,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -scopeguard 1.1.0 - MIT/Apache-2.0 +scopeguard 1.2.0 - MIT OR Apache-2.0 https://github.com/bluss/scopeguard Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers @@ -7800,36 +7792,6 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -scratch 1.0.7 - MIT OR Apache-2.0 -https://github.com/dtolnay/scratch - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - secret-service 3.0.1 - MIT OR Apache-2.0 https://github.com/hwchen/secret-service-rs @@ -7862,7 +7824,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -security-framework 2.7.0 - MIT OR Apache-2.0 +security-framework 2.11.0 - MIT OR Apache-2.0 https://github.com/kornelski/rust-security-framework The MIT License (MIT) @@ -7889,7 +7851,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -security-framework-sys 2.6.1 - MIT OR Apache-2.0 +security-framework-sys 2.11.0 - MIT OR Apache-2.0 https://github.com/kornelski/rust-security-framework The MIT License (MIT) @@ -7916,7 +7878,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -serde 1.0.163 - MIT OR Apache-2.0 +serde 1.0.202 - MIT OR Apache-2.0 https://github.com/serde-rs/serde Permission is hereby granted, free of charge, to any @@ -7946,7 +7908,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -serde_bytes 0.11.9 - MIT OR Apache-2.0 +serde_bytes 0.11.14 - MIT OR Apache-2.0 https://github.com/serde-rs/bytes Permission is hereby granted, free of charge, to any @@ -7976,7 +7938,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -serde_derive 1.0.163 - MIT OR Apache-2.0 +serde_derive 1.0.202 - MIT OR Apache-2.0 https://github.com/serde-rs/serde Permission is hereby granted, free of charge, to any @@ -8006,7 +7968,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -serde_json 1.0.96 - MIT OR Apache-2.0 +serde_json 1.0.117 - MIT OR Apache-2.0 https://github.com/serde-rs/json Permission is hereby granted, free of charge, to any @@ -8036,7 +7998,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -serde_repr 0.1.9 - MIT OR Apache-2.0 +serde_repr 0.1.19 - MIT OR Apache-2.0 https://github.com/dtolnay/serde-repr Permission is hereby granted, free of charge, to any @@ -8098,7 +8060,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -sha1 0.10.5 - MIT OR Apache-2.0 +sha1 0.10.6 - MIT OR Apache-2.0 https://github.com/RustCrypto/hashes All crates in this repository are licensed under either of @@ -8193,7 +8155,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- -sha2 0.10.6 - MIT OR Apache-2.0 +sha2 0.10.8 - MIT OR Apache-2.0 https://github.com/RustCrypto/hashes All crates in this repository are licensed under either of @@ -8352,7 +8314,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -signal-hook 0.3.15 - Apache-2.0/MIT +signal-hook-registry 1.4.2 - Apache-2.0/MIT https://github.com/vorner/signal-hook Copyright (c) 2017 tokio-jsonrpc developers @@ -8384,39 +8346,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -signal-hook-registry 1.4.0 - Apache-2.0/MIT -https://github.com/vorner/signal-hook - -Copyright (c) 2017 tokio-jsonrpc developers - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -slab 0.4.7 - MIT +slab 0.4.9 - MIT https://github.com/tokio-rs/slab The MIT License (MIT) @@ -8450,7 +8380,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -smallvec 1.10.0 - MIT OR Apache-2.0 +smallvec 1.13.2 - MIT OR Apache-2.0 https://github.com/servo/rust-smallvec Copyright (c) 2018 The Servo Project Developers @@ -8482,7 +8412,8 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -socket2 0.4.9 - MIT OR Apache-2.0 +socket2 0.4.10 - MIT OR Apache-2.0 +socket2 0.5.7 - MIT OR Apache-2.0 https://github.com/rust-lang/socket2 Copyright (c) 2014 Alex Crichton @@ -8542,7 +8473,7 @@ SOFTWARE. --------------------------------------------------------- -strsim 0.10.0 - MIT +strsim 0.11.1 - MIT https://github.com/rapidfuzz/strsim-rs The MIT License (MIT) @@ -8572,10 +8503,11 @@ SOFTWARE. --------------------------------------------------------- -subtle 2.4.1 - BSD-3-Clause +subtle 2.5.0 - BSD-3-Clause https://github.com/dalek-cryptography/subtle Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -8607,8 +8539,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --------------------------------------------------------- -syn 1.0.103 - MIT OR Apache-2.0 -syn 2.0.18 - MIT OR Apache-2.0 +syn 1.0.109 - MIT OR Apache-2.0 +syn 2.0.65 - MIT OR Apache-2.0 https://github.com/dtolnay/syn Permission is hereby granted, free of charge, to any @@ -8638,7 +8570,190 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -sysinfo 0.29.0 - MIT +sync_wrapper 0.1.2 - Apache-2.0 +https://github.com/Actyx/sync_wrapper + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +--------------------------------------------------------- + +--------------------------------------------------------- + +sysinfo 0.29.11 - MIT https://github.com/GuillaumeGomez/sysinfo The MIT License (MIT) @@ -8730,7 +8845,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tar 0.4.38 - MIT/Apache-2.0 +tar 0.4.40 - MIT/Apache-2.0 https://github.com/alexcrichton/tar-rs Copyright (c) 2014 Alex Crichton @@ -8762,7 +8877,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tempfile 3.5.0 - MIT OR Apache-2.0 +tempfile 3.10.1 - MIT OR Apache-2.0 https://github.com/Stebalien/tempfile Copyright (c) 2015 Steven Allen @@ -8794,35 +8909,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -termcolor 1.2.0 - Unlicense OR MIT -https://github.com/BurntSushi/termcolor - -The MIT License (MIT) - -Copyright (c) 2015 Andrew Gallant - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -thiserror 1.0.40 - MIT OR Apache-2.0 +thiserror 1.0.61 - MIT OR Apache-2.0 https://github.com/dtolnay/thiserror Permission is hereby granted, free of charge, to any @@ -8852,7 +8939,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -thiserror-impl 1.0.40 - MIT OR Apache-2.0 +thiserror-impl 1.0.61 - MIT OR Apache-2.0 https://github.com/dtolnay/thiserror Permission is hereby granted, free of charge, to any @@ -8882,7 +8969,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -time 0.3.21 - MIT OR Apache-2.0 +time 0.3.36 - MIT OR Apache-2.0 https://github.com/time-rs/time Copyright (c) 2024 Jacob Pratt et al. @@ -8908,7 +8995,7 @@ SOFTWARE. --------------------------------------------------------- -time-core 0.1.1 - MIT OR Apache-2.0 +time-core 0.1.2 - MIT OR Apache-2.0 https://github.com/time-rs/time Copyright (c) 2024 Jacob Pratt et al. @@ -8946,7 +9033,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -tinyvec_macros 0.1.0 - MIT OR Apache-2.0 OR Zlib +tinyvec_macros 0.1.1 - MIT OR Apache-2.0 OR Zlib https://github.com/Soveu/tinyvec_macros MIT License @@ -8974,70 +9061,58 @@ SOFTWARE. --------------------------------------------------------- -tokio 1.28.2 - MIT +tokio 1.37.0 - MIT https://github.com/tokio-rs/tokio -The MIT License (MIT) +MIT License -Copyright (c) 2023 Tokio Contributors +Copyright (c) Tokio Contributors -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -tokio-macros 2.1.0 - MIT +tokio-macros 2.2.0 - MIT https://github.com/tokio-rs/tokio -The MIT License (MIT) +MIT License -Copyright (c) 2023 Tokio Contributors +Copyright (c) Tokio Contributors -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- @@ -9076,36 +9151,30 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tokio-stream 0.1.11 - MIT +tokio-stream 0.1.15 - MIT https://github.com/tokio-rs/tokio -The MIT License (MIT) +MIT License -Copyright (c) 2023 Tokio Contributors +Copyright (c) Tokio Contributors -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- @@ -9139,41 +9208,61 @@ THE SOFTWARE. --------------------------------------------------------- -tokio-util 0.7.8 - MIT +tokio-util 0.7.11 - MIT https://github.com/tokio-rs/tokio -The MIT License (MIT) +MIT License -Copyright (c) 2023 Tokio Contributors +Copyright (c) Tokio Contributors -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -toml 0.5.9 - MIT/Apache-2.0 +toml_datetime 0.6.6 - MIT OR Apache-2.0 +https://github.com/toml-rs/toml + +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--------------------------------------------------------- + +--------------------------------------------------------- + +toml_edit 0.19.15 - MIT OR Apache-2.0 https://github.com/toml-rs/toml Copyright (c) Individual contributors @@ -9233,7 +9322,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tracing 0.1.37 - MIT +tracing 0.1.40 - MIT https://github.com/tokio-rs/tracing The MIT License (MIT) @@ -9267,7 +9356,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tracing-attributes 0.1.23 - MIT +tracing-attributes 0.1.27 - MIT https://github.com/tokio-rs/tracing The MIT License (MIT) @@ -9301,7 +9390,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -tracing-core 0.1.30 - MIT +tracing-core 0.1.32 - MIT https://github.com/tokio-rs/tracing The MIT License (MIT) @@ -9335,7 +9424,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -try-lock 0.2.3 - MIT +try-lock 0.2.5 - MIT https://github.com/seanmonstar/try-lock The MIT License (MIT) @@ -9419,7 +9508,7 @@ MIT License --------------------------------------------------------- -typenum 1.15.0 - MIT OR Apache-2.0 +typenum 1.17.0 - MIT OR Apache-2.0 https://github.com/paholg/typenum MIT OR Apache-2.0 @@ -9427,7 +9516,7 @@ MIT OR Apache-2.0 --------------------------------------------------------- -uds_windows 1.0.2 - MIT +uds_windows 1.1.0 - MIT https://github.com/haraldh/rust_uds_windows MIT License @@ -9455,7 +9544,7 @@ MIT License --------------------------------------------------------- -unicode-bidi 0.3.8 - MIT OR Apache-2.0 +unicode-bidi 0.3.15 - MIT OR Apache-2.0 https://github.com/servo/unicode-bidi Copyright (c) 2015 The Rust Project Developers @@ -9487,7 +9576,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-ident 1.0.5 - (MIT OR Apache-2.0) AND Unicode-DFS-2016 +unicode-ident 1.0.12 - (MIT OR Apache-2.0) AND Unicode-DFS-2016 https://github.com/dtolnay/unicode-ident Permission is hereby granted, free of charge, to any @@ -9517,7 +9606,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-normalization 0.1.22 - MIT/Apache-2.0 +unicode-normalization 0.1.23 - MIT/Apache-2.0 https://github.com/unicode-rs/unicode-normalization Copyright (c) 2015 The Rust Project Developers @@ -9549,7 +9638,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -unicode-width 0.1.10 - MIT/Apache-2.0 +unicode-width 0.1.12 - MIT OR Apache-2.0 https://github.com/unicode-rs/unicode-width Copyright (c) 2015 The Rust Project Developers @@ -9613,7 +9702,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -url 2.3.1 - MIT OR Apache-2.0 +url 2.5.0 - MIT OR Apache-2.0 https://github.com/servo/rust-url Copyright (c) 2013-2022 The rust-url developers @@ -9736,7 +9825,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -uuid 1.4.1 - Apache-2.0 OR MIT +uuid 1.8.0 - Apache-2.0 OR MIT https://github.com/uuid-rs/uuid Copyright (c) 2014 The Rust Project Developers @@ -9827,7 +9916,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -waker-fn 1.1.0 - Apache-2.0 OR MIT +waker-fn 1.2.0 - Apache-2.0 OR MIT https://github.com/smol-rs/waker-fn Permission is hereby granted, free of charge, to any @@ -9857,7 +9946,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -want 0.3.0 - MIT +want 0.3.1 - MIT https://github.com/seanmonstar/want The MIT License (MIT) @@ -9887,7 +9976,7 @@ THE SOFTWARE. wasi 0.11.0+wasi-snapshot-preview1 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT wasi 0.9.0+wasi-snapshot-preview1 - Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT -https://github.com/bytecodealliance/wasi +https://github.com/bytecodealliance/wasi-rs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9916,7 +10005,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen 0.2.83 - MIT/Apache-2.0 +wasm-bindgen 0.2.92 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen Copyright (c) 2014 Alex Crichton @@ -9948,7 +10037,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen-backend 0.2.83 - MIT/Apache-2.0 +wasm-bindgen-backend 0.2.92 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/backend Copyright (c) 2014 Alex Crichton @@ -9980,7 +10069,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen-futures 0.4.33 - MIT/Apache-2.0 +wasm-bindgen-futures 0.4.42 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/futures Copyright (c) 2014 Alex Crichton @@ -10012,7 +10101,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen-macro 0.2.83 - MIT/Apache-2.0 +wasm-bindgen-macro 0.2.92 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro Copyright (c) 2014 Alex Crichton @@ -10044,7 +10133,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen-macro-support 0.2.83 - MIT/Apache-2.0 +wasm-bindgen-macro-support 0.2.92 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro-support Copyright (c) 2014 Alex Crichton @@ -10076,7 +10165,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-bindgen-shared 0.2.83 - MIT/Apache-2.0 +wasm-bindgen-shared 0.2.92 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/shared Copyright (c) 2014 Alex Crichton @@ -10108,7 +10197,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wasm-streams 0.3.0 - MIT OR Apache-2.0 +wasm-streams 0.4.0 - MIT OR Apache-2.0 https://github.com/MattiasBuelens/wasm-streams/ Permission is hereby granted, free of charge, to any @@ -10138,7 +10227,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -web-sys 0.3.60 - MIT/Apache-2.0 +web-sys 0.3.69 - MIT OR Apache-2.0 https://github.com/rustwasm/wasm-bindgen/tree/master/crates/web-sys Copyright (c) 2014 Alex Crichton @@ -10170,34 +10259,6 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -wepoll-ffi 0.1.2 - MIT OR Apache-2.0 OR BSD-2-Clause -https://github.com/aclysma/wepoll-ffi - -MIT License - -Copyright (c) 2019-2020 Philip Degarmo and other wepoll-ffi contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - winapi 0.3.9 - MIT/Apache-2.0 https://github.com/retep998/winapi-rs @@ -10250,34 +10311,6 @@ SOFTWARE. --------------------------------------------------------- -winapi-util 0.1.5 - Unlicense/MIT -https://github.com/BurntSushi/winapi-util - -The MIT License (MIT) - -Copyright (c) 2017 Andrew Gallant - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - winapi-x86_64-pc-windows-gnu 0.4.0 - MIT/Apache-2.0 https://github.com/retep998/winapi-rs @@ -10304,7 +10337,7 @@ SOFTWARE. --------------------------------------------------------- -windows 0.48.0 - MIT OR Apache-2.0 +windows-core 0.52.0 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10332,9 +10365,8 @@ MIT License --------------------------------------------------------- -windows-sys 0.36.1 - MIT OR Apache-2.0 -windows-sys 0.45.0 - MIT OR Apache-2.0 windows-sys 0.48.0 - MIT OR Apache-2.0 +windows-sys 0.52.0 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10362,8 +10394,8 @@ MIT License --------------------------------------------------------- -windows-targets 0.42.2 - MIT OR Apache-2.0 -windows-targets 0.48.0 - MIT OR Apache-2.0 +windows-targets 0.48.5 - MIT OR Apache-2.0 +windows-targets 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10391,8 +10423,8 @@ MIT License --------------------------------------------------------- -windows_aarch64_gnullvm 0.42.2 - MIT OR Apache-2.0 -windows_aarch64_gnullvm 0.48.0 - MIT OR Apache-2.0 +windows_aarch64_gnullvm 0.48.5 - MIT OR Apache-2.0 +windows_aarch64_gnullvm 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10420,9 +10452,8 @@ MIT License --------------------------------------------------------- -windows_aarch64_msvc 0.36.1 - MIT OR Apache-2.0 -windows_aarch64_msvc 0.42.2 - MIT OR Apache-2.0 -windows_aarch64_msvc 0.48.0 - MIT OR Apache-2.0 +windows_aarch64_msvc 0.48.5 - MIT OR Apache-2.0 +windows_aarch64_msvc 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10450,9 +10481,8 @@ MIT License --------------------------------------------------------- -windows_i686_gnu 0.36.1 - MIT OR Apache-2.0 -windows_i686_gnu 0.42.2 - MIT OR Apache-2.0 -windows_i686_gnu 0.48.0 - MIT OR Apache-2.0 +windows_i686_gnu 0.48.5 - MIT OR Apache-2.0 +windows_i686_gnu 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10480,9 +10510,7 @@ MIT License --------------------------------------------------------- -windows_i686_msvc 0.36.1 - MIT OR Apache-2.0 -windows_i686_msvc 0.42.2 - MIT OR Apache-2.0 -windows_i686_msvc 0.48.0 - MIT OR Apache-2.0 +windows_i686_gnullvm 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10510,9 +10538,8 @@ MIT License --------------------------------------------------------- -windows_x86_64_gnu 0.36.1 - MIT OR Apache-2.0 -windows_x86_64_gnu 0.42.2 - MIT OR Apache-2.0 -windows_x86_64_gnu 0.48.0 - MIT OR Apache-2.0 +windows_i686_msvc 0.48.5 - MIT OR Apache-2.0 +windows_i686_msvc 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10540,8 +10567,8 @@ MIT License --------------------------------------------------------- -windows_x86_64_gnullvm 0.42.2 - MIT OR Apache-2.0 -windows_x86_64_gnullvm 0.48.0 - MIT OR Apache-2.0 +windows_x86_64_gnu 0.48.5 - MIT OR Apache-2.0 +windows_x86_64_gnu 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10569,9 +10596,8 @@ MIT License --------------------------------------------------------- -windows_x86_64_msvc 0.36.1 - MIT OR Apache-2.0 -windows_x86_64_msvc 0.42.2 - MIT OR Apache-2.0 -windows_x86_64_msvc 0.48.0 - MIT OR Apache-2.0 +windows_x86_64_gnullvm 0.48.5 - MIT OR Apache-2.0 +windows_x86_64_gnullvm 0.52.5 - MIT OR Apache-2.0 https://github.com/microsoft/windows-rs MIT License @@ -10599,7 +10625,36 @@ MIT License --------------------------------------------------------- -winnow 0.4.1 - MIT +windows_x86_64_msvc 0.48.5 - MIT OR Apache-2.0 +windows_x86_64_msvc 0.52.5 - MIT OR Apache-2.0 +https://github.com/microsoft/windows-rs + +MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +--------------------------------------------------------- + +--------------------------------------------------------- + +winnow 0.5.40 - MIT https://github.com/winnow-rs/winnow The MIT License (MIT) @@ -10655,7 +10710,7 @@ THE SOFTWARE. --------------------------------------------------------- -xattr 0.2.3 - MIT/Apache-2.0 +xattr 1.3.1 - MIT/Apache-2.0 https://github.com/Stebalien/xattr Copyright (c) 2015 Steven Allen @@ -10687,7 +10742,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -xdg-home 1.0.0 - MIT +xdg-home 1.1.0 - MIT https://github.com/zeenix/xdg-home The MIT License (MIT) @@ -10733,109 +10788,31 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- -zbus 3.13.1 - MIT +zbus 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- -zbus_macros 3.13.1 - MIT +zbus_macros 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- -zbus_names 2.5.1 - MIT +zbus_names 2.6.1 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- -zeroize 1.3.0 - Apache-2.0 OR MIT +zeroize 1.7.0 - Apache-2.0 OR MIT https://github.com/RustCrypto/utils/tree/master/zeroize All crates licensed under either of @@ -10890,7 +10867,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted --------------------------------------------------------- zip 0.6.6 - MIT -https://github.com/Pr0methean/zip +https://github.com/zip-rs/zip2 The MIT License (MIT) @@ -10913,74 +10890,25 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Some files in the "tests/data" subdirectory of this repository are under other +licences; see files named LICENSE.*.txt for details. --------------------------------------------------------- --------------------------------------------------------- -zvariant 3.14.0 - MIT +zvariant 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- -zvariant_derive 3.14.0 - MIT +zvariant_derive 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -10988,31 +10916,5 @@ DEALINGS IN THE SOFTWARE. zvariant_utils 1.0.1 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- \ No newline at end of file diff --git a/cli/src/async_pipe.rs b/cli/src/async_pipe.rs index e9b710c1d68..78aed6fe3e7 100644 --- a/cli/src/async_pipe.rs +++ b/cli/src/async_pipe.rs @@ -227,7 +227,7 @@ impl hyper::server::accept::Accept for PollableAsyncListener { } } -/// Gets a random name for a pipe/socket on the paltform +/// Gets a random name for a pipe/socket on the platform pub fn get_socket_name() -> PathBuf { cfg_if::cfg_if! { if #[cfg(unix)] { diff --git a/cli/src/auth.rs b/cli/src/auth.rs index 67f1bfa6bc7..2d9162c5483 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -287,7 +287,7 @@ impl StorageImplementation for ThreadKeyringStorage { #[derive(Default)] struct KeyringStorage { - // keywring storage can be split into multiple entries due to entry length limits + // keyring storage can be split into multiple entries due to entry length limits // on Windows https://github.com/microsoft/vscode-cli/issues/358 entries: Vec, } diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 79c4d3767a1..05e22e0cfb3 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -64,7 +64,7 @@ pub struct IntegratedCli { pub core: CliCore, } -/// Common CLI shared between intergated and standalone interfaces. +/// Common CLI shared between integrated and standalone interfaces. #[derive(Args, Debug, Default, Clone)] pub struct CliCore { /// One or more files, folders, or URIs to open. @@ -619,7 +619,7 @@ pub enum OutputFormat { #[derive(Args, Clone, Debug, Default)] pub struct ExistingTunnelArgs { /// Name you'd like to assign preexisting tunnel to use to connect the tunnel - /// Old option, new code sohuld just use `--name`. + /// Old option, new code should just use `--name`. #[clap(long, hide = true)] pub tunnel_name: Option, diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index fba92723426..12c0cdafec9 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -12,7 +12,6 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use const_format::concatcp; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -56,16 +55,9 @@ const RELEASE_CACHE_SECS: u64 = 60 * 60; /// Number of bytes for the secret keys. See workbench.ts for their usage. const SECRET_KEY_BYTES: usize = 32; /// Path to mint the key combining server and client parts. -const SECRET_KEY_MINT_PATH: &str = "/_vscode-cli/mint-key"; +const SECRET_KEY_MINT_PATH: &str = "_vscode-cli/mint-key"; /// Cookie set to the `SECRET_KEY_MINT_PATH` const PATH_COOKIE_NAME: &str = "vscode-secret-key-path"; -/// Cookie set to the `SECRET_KEY_MINT_PATH` -const PATH_COOKIE_VALUE: &str = concatcp!( - PATH_COOKIE_NAME, - "=", - SECRET_KEY_MINT_PATH, - "; SameSite=Strict; Path=/" -); /// HTTP-only cookie where the client's secret half is stored. const SECRET_KEY_COOKIE_NAME: &str = "vscode-cli-secret-half"; @@ -158,17 +150,22 @@ struct HandleContext { /// Handler function for an inbound request async fn handle(ctx: HandleContext, req: Request) -> Result, Infallible> { let client_key_half = get_client_key_half(&req); - let mut res = match req.uri().path() { - SECRET_KEY_MINT_PATH => handle_secret_mint(ctx, req), - _ => handle_proxied(ctx, req).await, + let path = req.uri().path(); + + let mut res = if path.starts_with(&ctx.cm.base_path) + && path.get(ctx.cm.base_path.len()..).unwrap_or_default() == SECRET_KEY_MINT_PATH + { + handle_secret_mint(&ctx, req) + } else { + handle_proxied(&ctx, req).await }; - append_secret_headers(&mut res, &client_key_half); + append_secret_headers(&ctx.cm.base_path, &mut res, &client_key_half); Ok(res) } -async fn handle_proxied(ctx: HandleContext, req: Request) -> Response { +async fn handle_proxied(ctx: &HandleContext, req: Request) -> Response { let release = if let Some((r, _)) = get_release_from_path(req.uri().path(), ctx.cm.platform) { r } else { @@ -194,7 +191,7 @@ async fn handle_proxied(ctx: HandleContext, req: Request) -> Response) -> Response { +fn handle_secret_mint(ctx: &HandleContext, req: Request) -> Response { use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); @@ -208,11 +205,20 @@ fn handle_secret_mint(ctx: HandleContext, req: Request) -> Response /// Appends headers to response to maintain the secret storage of the workbench: /// sets the `PATH_COOKIE_VALUE` so workbench.ts knows about the 'mint' endpoint, /// and maintains the http-only cookie the client will use for cookies. -fn append_secret_headers(res: &mut Response, client_key_half: &SecretKeyPart) { +fn append_secret_headers( + base_path: &str, + res: &mut Response, + client_key_half: &SecretKeyPart, +) { let headers = res.headers_mut(); headers.append( hyper::header::SET_COOKIE, - PATH_COOKIE_VALUE.parse().unwrap(), + format!( + "{}={}{}; SameSite=Strict; Path=/", + PATH_COOKIE_NAME, base_path, SECRET_KEY_MINT_PATH, + ) + .parse() + .unwrap(), ); headers.append( hyper::header::SET_COOKIE, @@ -496,6 +502,8 @@ struct ConnectionManager { pub platform: Platform, pub log: log::Logger, args: ServeWebArgs, + /// Server base path, ending in `/` + base_path: String, /// Cache where servers are stored cache: DownloadCache, /// Mapping of (Quality, Commit) to the state each server is in @@ -510,11 +518,24 @@ fn key_for_release(release: &Release) -> (Quality, String) { (release.quality, release.commit.clone()) } +fn normalize_base_path(p: &str) -> String { + let p = p.trim_matches('/'); + + if p.is_empty() { + return "/".to_string(); + } + + format!("/{}/", p.trim_matches('/')) +} + impl ConnectionManager { pub fn new(ctx: &CommandContext, platform: Platform, args: ServeWebArgs) -> Arc { + let base_path = normalize_base_path(args.server_base_path.as_deref().unwrap_or_default()); + Arc::new(Self { platform, args, + base_path, log: ctx.log.clone(), cache: DownloadCache::new(ctx.paths.web_server_storage()), update_service: UpdateService::new( diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 1755dbbfaef..688f603f593 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -562,7 +562,7 @@ async fn serve_with_csa( match acquire_singleton(&paths.tunnel_lockfile()).await { Ok(SingletonConnection::Client(stream)) => { debug!(log, "starting as client to singleton"); - if gateway_args.name.is_none() + if gateway_args.name.is_some() || !gateway_args.install_extension.is_empty() || gateway_args.tunnel.tunnel_id.is_some() { diff --git a/cli/src/constants.rs b/cli/src/constants.rs index 6f604e8876e..1e277a89d6a 100644 --- a/cli/src/constants.rs +++ b/cli/src/constants.rs @@ -13,7 +13,7 @@ use crate::options::Quality; pub const CONTROL_PORT: u16 = 31545; -/// Protocol version sent to clients. This can be used to indiciate new or +/// Protocol version sent to clients. This can be used to indicate new or /// changed capabilities that clients may wish to leverage. /// 1 - Initial protocol version /// 2 - Addition of `serve.compressed` property to control whether servermsg's diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index f42984cfac1..dfb5e381179 100644 --- a/cli/src/tunnels/control_server.rs +++ b/cli/src/tunnels/control_server.rs @@ -920,9 +920,14 @@ async fn handle_update( info!(log, "Updating CLI to {}", latest_release); - updater + let r = updater .do_update(&latest_release, SilentCopyProgress()) - .await?; + .await; + + if let Err(e) = r { + did_update.store(false, Ordering::SeqCst); + return Err(e); + } Ok(UpdateResult { up_to_date: true, diff --git a/cli/src/tunnels/dev_tunnels.rs b/cli/src/tunnels/dev_tunnels.rs index 19ee3c2bf42..a964b446384 100644 --- a/cli/src/tunnels/dev_tunnels.rs +++ b/cli/src/tunnels/dev_tunnels.rs @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use super::protocol::{self, PortPrivacy}; +use super::protocol::{self, PortPrivacy, PortProtocol}; use crate::auth; use crate::constants::{IS_INTERACTIVE_CLI, PROTOCOL_VERSION_TAG, TUNNEL_SERVICE_USER_AGENT}; use crate::state::{LauncherPaths, PersistedState}; @@ -221,8 +221,11 @@ impl ActiveTunnel { &self, port_number: u16, privacy: PortPrivacy, + protocol: PortProtocol, ) -> Result<(), AnyError> { - self.manager.add_port_tcp(port_number, privacy).await?; + self.manager + .add_port_tcp(port_number, privacy, protocol) + .await?; Ok(()) } @@ -972,13 +975,14 @@ impl ActiveTunnelManager { &self, port_number: u16, privacy: PortPrivacy, + protocol: PortProtocol, ) -> Result<(), WrappedError> { self.relay .lock() .await .add_port(&TunnelPort { port_number, - protocol: Some(TUNNEL_PROTOCOL_AUTO.to_owned()), + protocol: Some(protocol.to_contract_str().to_string()), access_control: Some(privacy_to_tunnel_acl(privacy)), ..Default::default() }) diff --git a/cli/src/tunnels/local_forwarding.rs b/cli/src/tunnels/local_forwarding.rs index e6410860cb0..93c2d244159 100644 --- a/cli/src/tunnels/local_forwarding.rs +++ b/cli/src/tunnels/local_forwarding.rs @@ -27,7 +27,7 @@ use super::{ protocol::{ self, forward_singleton::{PortList, SetPortsResponse}, - PortPrivacy, + PortPrivacy, PortProtocol, }, shutdown_signal::ShutdownSignal, }; @@ -71,8 +71,13 @@ impl PortCount { } } } +#[derive(Clone)] +struct PortMapRec { + count: PortCount, + protocol: PortProtocol, +} -type PortMap = HashMap; +type PortMap = HashMap; /// The PortForwardingHandle is given out to multiple consumers to allow /// them to set_ports that they want to be forwarded. @@ -99,8 +104,8 @@ impl PortForwardingSender { for p in current.iter() { if !ports.contains(p) { let n = v.get_mut(&p.number).expect("expected port in map"); - n[p.privacy] -= 1; - if n.is_empty() { + n.count[p.privacy] -= 1; + if n.count.is_empty() { v.remove(&p.number); } } @@ -110,12 +115,19 @@ impl PortForwardingSender { if !current.contains(p) { match v.get_mut(&p.number) { Some(n) => { - n[p.privacy] += 1; + n.count[p.privacy] += 1; + n.protocol = p.protocol; } None => { - let mut pc = PortCount::default(); - pc[p.privacy] += 1; - v.insert(p.number, pc); + let mut count = PortCount::default(); + count[p.privacy] += 1; + v.insert( + p.number, + PortMapRec { + count, + protocol: p.protocol, + }, + ); } }; } @@ -164,22 +176,34 @@ impl PortForwardingReceiver { while self.receiver.changed().await.is_ok() { let next = self.receiver.borrow().clone(); - for (port, count) in current.iter() { - let privacy = count.primary_privacy(); - if !matches!(next.get(port), Some(n) if n.primary_privacy() == privacy) { + for (port, rec) in current.iter() { + let privacy = rec.count.primary_privacy(); + if !matches!(next.get(port), Some(n) if n.count.primary_privacy() == privacy) { match tunnel.remove_port(*port).await { - Ok(_) => info!(log, "stopped forwarding port {} at {:?}", *port, privacy), - Err(e) => error!(log, "failed to stop forwarding port {}: {}", port, e), + Ok(_) => info!( + log, + "stopped forwarding {} port {} at {:?}", rec.protocol, *port, privacy + ), + Err(e) => error!( + log, + "failed to stop forwarding {} port {}: {}", rec.protocol, port, e + ), } } } - for (port, count) in next.iter() { - let privacy = count.primary_privacy(); - if !matches!(current.get(port), Some(n) if n.primary_privacy() == privacy) { - match tunnel.add_port_tcp(*port, privacy).await { - Ok(_) => info!(log, "forwarding port {} at {:?}", port, privacy), - Err(e) => error!(log, "failed to forward port {}: {}", port, e), + for (port, rec) in next.iter() { + let privacy = rec.count.primary_privacy(); + if !matches!(current.get(port), Some(n) if n.count.primary_privacy() == privacy) { + match tunnel.add_port_tcp(*port, privacy, rec.protocol).await { + Ok(_) => info!( + log, + "forwarding {} port {} at {:?}", rec.protocol, port, privacy + ), + Err(e) => error!( + log, + "failed to forward {} port {}: {}", rec.protocol, port, e + ), } } } diff --git a/cli/src/tunnels/port_forwarder.rs b/cli/src/tunnels/port_forwarder.rs index 30267e8bc86..b05ae95ae40 100644 --- a/cli/src/tunnels/port_forwarder.rs +++ b/cli/src/tunnels/port_forwarder.rs @@ -12,7 +12,10 @@ use crate::{ util::errors::{AnyError, CannotForwardControlPort, ServerHasClosed}, }; -use super::{dev_tunnels::ActiveTunnel, protocol::PortPrivacy}; +use super::{ + dev_tunnels::ActiveTunnel, + protocol::{PortPrivacy, PortProtocol}, +}; pub enum PortForwardingRec { Forward(u16, PortPrivacy, oneshot::Sender>), @@ -89,7 +92,9 @@ impl PortForwardingProcessor { } if !self.forwarded.contains(&port) { - tunnel.add_port_tcp(port, privacy).await?; + tunnel + .add_port_tcp(port, privacy, PortProtocol::Auto) + .await?; self.forwarded.insert(port); } diff --git a/cli/src/tunnels/protocol.rs b/cli/src/tunnels/protocol.rs index d26ea978068..3654826c57e 100644 --- a/cli/src/tunnels/protocol.rs +++ b/cli/src/tunnels/protocol.rs @@ -299,10 +299,40 @@ pub enum PortPrivacy { Private, } +#[derive(Serialize, Deserialize, PartialEq, Copy, Eq, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum PortProtocol { + Auto, + Http, + Https, +} + +impl std::fmt::Display for PortProtocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_contract_str()) + } +} + +impl Default for PortProtocol { + fn default() -> Self { + Self::Auto + } +} + +impl PortProtocol { + pub fn to_contract_str(&self) -> &'static str { + match *self { + Self::Auto => tunnels::contracts::TUNNEL_PROTOCOL_AUTO, + Self::Http => tunnels::contracts::TUNNEL_PROTOCOL_HTTP, + Self::Https => tunnels::contracts::TUNNEL_PROTOCOL_HTTPS, + } + } +} + pub mod forward_singleton { use serde::{Deserialize, Serialize}; - use super::PortPrivacy; + use super::{PortPrivacy, PortProtocol}; pub const METHOD_SET_PORTS: &str = "set_ports"; @@ -310,6 +340,7 @@ pub mod forward_singleton { pub struct PortRec { pub number: u16, pub privacy: PortPrivacy, + pub protocol: PortProtocol, } pub type PortList = Vec; diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 8a00fda49b8..f6bfd751895 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -11,7 +11,9 @@ "icon": "images/icon.png", "activationEvents": [ "onProfile", - "onProfile:github" + "onProfile:github", + "onLanguage:json", + "onLanguage:jsonc" ], "enabledApiProposals": [ "profileContentHandlers" diff --git a/extensions/cpp/package.json b/extensions/cpp/package.json index c1d3f4882f6..9f3c890a48b 100644 --- a/extensions/cpp/package.json +++ b/extensions/cpp/package.json @@ -30,9 +30,13 @@ "id": "cpp", "extensions": [ ".cpp", + ".cppm", ".cc", + ".ccm", ".cxx", + ".cxxm", ".c++", + ".c++m", ".hpp", ".hh", ".hxx", diff --git a/extensions/csharp/cgmanifest.json b/extensions/csharp/cgmanifest.json index 1c88bd17296..58a7408dbbe 100644 --- a/extensions/csharp/cgmanifest.json +++ b/extensions/csharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/csharp-tmLanguage", "repositoryUrl": "https://github.com/dotnet/csharp-tmLanguage", - "commitHash": "7a7482ffc72a6677a87eb1ed76005593a4f7f131" + "commitHash": "d63e2661d4e0c83b6c7810eb1d0eedc5da843b04" } }, "license": "MIT", diff --git a/extensions/csharp/syntaxes/csharp.tmLanguage.json b/extensions/csharp/syntaxes/csharp.tmLanguage.json index 96dbe04473c..4a2497a064a 100644 --- a/extensions/csharp/syntaxes/csharp.tmLanguage.json +++ b/extensions/csharp/syntaxes/csharp.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/dotnet/csharp-tmLanguage/commit/7a7482ffc72a6677a87eb1ed76005593a4f7f131", + "version": "https://github.com/dotnet/csharp-tmLanguage/commit/d63e2661d4e0c83b6c7810eb1d0eedc5da843b04", "name": "C#", "scopeName": "source.cs", "patterns": [ @@ -512,6 +512,9 @@ { "include": "#type-name" }, + { + "include": "#type-arguments" + }, { "include": "#attribute-arguments" } diff --git a/extensions/css-language-features/client/src/browser/cssClientMain.ts b/extensions/css-language-features/client/src/browser/cssClientMain.ts index 6522c786389..c89997ffaa0 100644 --- a/extensions/css-language-features/client/src/browser/cssClientMain.ts +++ b/extensions/css-language-features/client/src/browser/cssClientMain.ts @@ -8,13 +8,6 @@ import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient import { startClient, LanguageClientConstructor } from '../cssClient'; import { LanguageClient } from 'vscode-languageclient/browser'; -declare const Worker: { - new(stringUrl: string): any; -}; -declare const TextDecoder: { - new(encoding?: string): { decode(buffer: ArrayBuffer): string }; -}; - let client: BaseLanguageClient | undefined; // this method is called when vs code is activated @@ -25,7 +18,7 @@ export async function activate(context: ExtensionContext) { worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' }); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, clientOptions, worker); + return new LanguageClient(id, name, worker, clientOptions); }; client = await startClient(context, newLanguageClient, { TextDecoder }); diff --git a/extensions/css-language-features/client/tsconfig.json b/extensions/css-language-features/client/tsconfig.json index 573b24b4aa6..44b77895c10 100644 --- a/extensions/css-language-features/client/tsconfig.json +++ b/extensions/css-language-features/client/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./out" + "outDir": "./out", + "lib": [ + "webworker" + ] }, "include": [ "src/**/*", diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index f4f6adfb7f4..4ddfe8fce0d 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -994,7 +994,7 @@ ] }, "dependencies": { - "vscode-languageclient": "^10.0.0-next.5", + "vscode-languageclient": "^10.0.0-next.8", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 0f1750e800a..fe4f64d7c01 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -11,8 +11,8 @@ "browser": "./dist/browser/cssServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.2.14", - "vscode-languageserver": "^10.0.0-next.3", + "vscode-css-languageservice": "^6.3.0", + "vscode-languageserver": "^10.0.0-next.6", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index 8d4c46d641e..59033f770c1 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -24,28 +24,28 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-css-languageservice@^6.2.14: - version "6.2.14" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.2.14.tgz#d44fe75c03942d865a9c1a5ff5fb4e8dec1f89d0" - integrity sha512-5UPQ9Y1sUTnuMyaMBpO7LrBkqjhEJb5eAwdUlDp+Uez8lry+Tspnk3+3p2qWS4LlNsr4p3v9WkZxUf1ltgFpgw== +vscode-css-languageservice@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.3.0.tgz#51724d193d19b1a9075b1cef5cfeea6a555d2aa4" + integrity sha512-nU92imtkgzpCL0xikrIb8WvedV553F2BENzgz23wFuok/HLN5BeQmroMy26pUwFxV2eV8oNRmYCUv8iO7kSMhw== dependencies: "@vscode/l10n" "^0.0.18" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageserver-protocol@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.4.tgz#3c56f6eb588bb42fccc0ac54a0d5daf2d02f0a1b" - integrity sha512-/2bleKBxZLyRObS4mkpaWlVI9xGiUqMVmh/ztZ2vL4uP2XyIpraT45JBpn9AtXr0alqKJPKLuKr+/qcYULvm/w== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" @@ -57,17 +57,17 @@ vscode-languageserver-types@3.17.5: resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== -vscode-languageserver@^10.0.0-next.3: - version "10.0.0-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.3.tgz#a63c5ea9fab1be93d7732ab0fdc18c9b37956e07" - integrity sha512-4x1qHImf6ePji4+8PX43lnBCBfBNdi2jneGX2k5FswJhx/cxaYYmusShmmtO/clyL1iurxJacrQoXfw9+ikhvg== +vscode-languageserver@^10.0.0-next.6: + version "10.0.0-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.6.tgz#0db118a93fe010c6b40cd04e91a15d09e7b60b60" + integrity sha512-0Lh1nhQfSxo5Ob+ayYO1QTIsDix2/Lc72Urm1KZrCFxK5zIFYaEh3QFeM9oZih4Rzs0ZkQPXXnoHtpvs5GT+Zw== dependencies: - vscode-languageserver-protocol "3.17.6-next.4" + vscode-languageserver-protocol "3.17.6-next.6" vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index 25a22d07ca6..eef1c9ab57d 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -47,32 +47,32 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageclient@^10.0.0-next.5: - version "10.0.0-next.5" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.5.tgz#7431d88255a5fd99e9423659ac484b1f968200f3" - integrity sha512-JIf1WE7fvV0RElFM062bAummI433vcxuFwqoYAp+1zTVhta/jznxkTz1zs3Hbj2tiDfclf0TZ0qCxflAP1mY2Q== +vscode-languageclient@^10.0.0-next.8: + version "10.0.0-next.8" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.8.tgz#5afa0ced3b2ac68d31cc1c48edc4f289744542a0" + integrity sha512-D9inIHgqKayO9Tv0MeLb3XIL76yTuWmKdHqcGZKzjtQrMGJgASJDYWTapu+yAjEpDp0gmVOaCYyIlLB86ncDoQ== dependencies: minimatch "^9.0.3" semver "^7.6.0" - vscode-languageserver-protocol "3.17.6-next.4" + vscode-languageserver-protocol "3.17.6-next.6" -vscode-languageserver-protocol@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.4.tgz#3c56f6eb588bb42fccc0ac54a0d5daf2d02f0a1b" - integrity sha512-/2bleKBxZLyRObS4mkpaWlVI9xGiUqMVmh/ztZ2vL4uP2XyIpraT45JBpn9AtXr0alqKJPKLuKr+/qcYULvm/w== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/extension-editing/src/extensionLinter.ts b/extensions/extension-editing/src/extensionLinter.ts index dd1727edb7b..b69dac0e2dd 100644 --- a/extensions/extension-editing/src/extensionLinter.ts +++ b/extensions/extension-editing/src/extensionLinter.ts @@ -149,7 +149,8 @@ export class ExtensionLinter { const effectiveProposalNames = extensionEnabledApiProposals[extensionId]; if (Array.isArray(effectiveProposalNames) && enabledApiProposals.children) { for (const child of enabledApiProposals.children) { - if (child.type === 'string' && !effectiveProposalNames.includes(getNodeValue(child))) { + const proposalName = child.type === 'string' ? getNodeValue(child) : undefined; + if (typeof proposalName === 'string' && !effectiveProposalNames.includes(proposalName.split('@')[0])) { const start = document.positionAt(child.offset); const end = document.positionAt(child.offset + child.length); diagnostics.push(new Diagnostic(new Range(start, end), apiProposalNotListed, DiagnosticSeverity.Error)); diff --git a/extensions/fsharp/cgmanifest.json b/extensions/fsharp/cgmanifest.json index 524b3fa0d46..0b3c5d112e5 100644 --- a/extensions/fsharp/cgmanifest.json +++ b/extensions/fsharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "ionide/ionide-fsgrammar", "repositoryUrl": "https://github.com/ionide/ionide-fsgrammar", - "commitHash": "7d029a46f17637228b2ee85dd02e511c3e8039b3" + "commitHash": "0100f551f6c32598a58aba97344bf828673fec7a" } }, "license": "MIT", diff --git a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json b/extensions/fsharp/syntaxes/fsharp.tmLanguage.json index 5063f1c5210..7806c100eae 100644 --- a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json +++ b/extensions/fsharp/syntaxes/fsharp.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/ionide/ionide-fsgrammar/commit/7d029a46f17637228b2ee85dd02e511c3e8039b3", + "version": "https://github.com/ionide/ionide-fsgrammar/commit/0100f551f6c32598a58aba97344bf828673fec7a", "name": "fsharp", "scopeName": "source.fsharp", "patterns": [ @@ -617,7 +617,7 @@ }, { "name": "constant.numeric.float.fsharp", - "match": "\\b-?[0-9][0-9_]*((\\.([0-9][0-9_]*([eE][+-]??[0-9][0-9_]*)?)?)|([eE][+-]??[0-9][0-9_]*))" + "match": "\\b-?[0-9][0-9_]*((\\.(?!\\.)([0-9][0-9_]*([eE][+-]??[0-9][0-9_]*)?)?)|([eE][+-]??[0-9][0-9_]*))" }, { "name": "constant.numeric.integer.nativeint.fsharp", @@ -635,7 +635,7 @@ }, "abstract_definition": { "name": "abstract.definition.fsharp", - "begin": "\\b(abstract)\\s+(member)?(\\s+\\[\\<.*\\>\\])?\\s*([_[:alpha:]0-9,\\._`\\s]+)(<)?", + "begin": "\\b(static)?\\s+(abstract)\\s+(member)?(\\s+\\[\\<.*\\>\\])?\\s*([_[:alpha:]0-9,\\._`\\s]+)(<)?", "end": "\\s*(with)\\b|=|$", "beginCaptures": { "1": { @@ -645,6 +645,9 @@ "name": "keyword.fsharp" }, "3": { + "name": "keyword.fsharp" + }, + "4": { "name": "support.function.attribute.fsharp" }, "5": { @@ -933,7 +936,7 @@ "patterns": [ { "name": "binding.fsharp", - "begin": "\\b(let mutable|static let mutable|static let|let inline|let|and|member val|static member inline|static member|default|member|override|let!)(\\s+rec|mutable)?(\\s+\\[\\<.*\\>\\])?\\s*(private|internal|public)?\\s+(\\[[^-=]*\\]|[_[:alpha:]]([_[:alpha:]0-9\\._]+)*|``[_[:alpha:]]([_[:alpha:]0-9\\._`\\s]+|(?<=,)\\s)*)?", + "begin": "\\b(let mutable|static let mutable|static let|let inline|let|and|member val|member inline|static member inline|static member|default|member|override|let!)(\\s+rec|mutable)?(\\s+\\[\\<.*\\>\\])?\\s*(private|internal|public)?\\s+(\\[[^-=]*\\]|[_[:alpha:]]([_[:alpha:]0-9\\._]+)*|``[_[:alpha:]]([_[:alpha:]0-9\\._`\\s]+|(?<=,)\\s)*)?", "end": "\\s*((with\\b)|(=|\\n+=|(?<=\\=)))", "beginCaptures": { "1": { @@ -1008,7 +1011,7 @@ }, { "name": "binding.fsharp", - "begin": "\\b(static val mutable|val mutable|val)(\\s+rec|mutable)?(\\s+\\[\\<.*\\>\\])?\\s*(private|internal|public)?\\s+(\\[[^-=]*\\]|[_[:alpha:]]([_[:alpha:]0-9,\\._]+)*|``[_[:alpha:]]([_[:alpha:]0-9,\\._`\\s]+|(?<=,)\\s)*)?", + "begin": "\\b(static val mutable|val mutable|val inline|val)(\\s+rec|mutable)?(\\s+\\[\\<.*\\>\\])?\\s*(private|internal|public)?\\s+(\\[[^-=]*\\]|[_[:alpha:]]([_[:alpha:]0-9,\\._]+)*|``[_[:alpha:]]([_[:alpha:]0-9,\\._`\\s]+|(?<=,)\\s)*)?", "end": "\\n$", "beginCaptures": { "1": { diff --git a/extensions/git/package.json b/extensions/git/package.json index dfbb29289db..f2445eb3978 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1967,23 +1967,23 @@ { "command": "git.pushRef", "group": "navigation", - "when": "scmProvider == git && scmHistoryItemGroupHasUpstream" + "when": "scmProvider == git && scmHistoryItemGroupHasRemote" }, { "command": "git.publish", "group": "navigation", - "when": "scmProvider == git && !scmHistoryItemGroupHasUpstream" + "when": "scmProvider == git && !scmHistoryItemGroupHasRemote" } ], "scm/outgoingChanges/context": [ { "command": "git.pushRef", - "when": "scmProvider == git && scmHistoryItemGroupHasUpstream", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote", "group": "1_modification@1" }, { "command": "git.publish", - "when": "scmProvider == git && !scmHistoryItemGroupHasUpstream", + "when": "scmProvider == git && !scmHistoryItemGroupHasRemote", "group": "1_modification@1" } ], @@ -3401,7 +3401,7 @@ "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", - "jschardet": "3.0.0", + "jschardet": "3.1.3", "picomatch": "2.3.1", "vscode-uri": "^2.0.0", "which": "4.0.0" diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index f049939c137..f94ecbab7b0 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -44,6 +44,7 @@ export class ApiRepositoryState implements RepositoryState { get mergeChanges(): Change[] { return this._repository.mergeGroup.resourceStates.map(r => new ApiChange(r)); } get indexChanges(): Change[] { return this._repository.indexGroup.resourceStates.map(r => new ApiChange(r)); } get workingTreeChanges(): Change[] { return this._repository.workingTreeGroup.resourceStates.map(r => new ApiChange(r)); } + get untrackedChanges(): Change[] { return this._repository.untrackedGroup.resourceStates.map(r => new ApiChange(r)); } readonly onDidChange: Event = this._repository.onDidRunGitStatus; diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 685b5413947..ce27e914244 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -122,6 +122,7 @@ export interface RepositoryState { readonly mergeChanges: Change[]; readonly indexChanges: Change[]; readonly workingTreeChanges: Change[]; + readonly untrackedChanges: Change[]; readonly onDidChange: Event; } @@ -144,6 +145,7 @@ export interface LogOptions { readonly sortByAuthorDate?: boolean; readonly shortStats?: boolean; readonly author?: string; + readonly refNames?: string[]; } export interface CommitOptions { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 48f67f396d4..686ce366e29 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1339,14 +1339,14 @@ export class CommandCenter { @command('git.stage') async stage(...resourceStates: SourceControlResourceState[]): Promise { - this.logger.debug(`git.stage ${resourceStates.length} `); + this.logger.debug(`[CommandCenter][stage] git.stage ${resourceStates.length} `); resourceStates = resourceStates.filter(s => !!s); if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) { const resource = this.getSCMResource(); - this.logger.debug(`git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null} `); + this.logger.debug(`[CommandCenter][stage] git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null} `); if (!resource) { return; @@ -1389,7 +1389,7 @@ export class CommandCenter { const untracked = selection.filter(s => s.resourceGroupType === ResourceGroupType.Untracked); const scmResources = [...workingTree, ...untracked, ...resolved, ...unresolved]; - this.logger.debug(`git.stage.scmResources ${scmResources.length} `); + this.logger.debug(`[CommandCenter][stage] git.stage.scmResources ${scmResources.length} `); if (!scmResources.length) { return; } @@ -2063,76 +2063,81 @@ export class CommandCenter { promptToSaveFilesBeforeCommit = 'never'; } - const enableSmartCommit = config.get('enableSmartCommit') === true; + let enableSmartCommit = config.get('enableSmartCommit') === true; const enableCommitSigning = config.get('enableCommitSigning') === true; let noStagedChanges = repository.indexGroup.resourceStates.length === 0; let noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; - if (promptToSaveFilesBeforeCommit !== 'never') { - let documents = workspace.textDocuments - .filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath)); + if (!opts.empty) { + if (promptToSaveFilesBeforeCommit !== 'never') { + let documents = workspace.textDocuments + .filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath)); - if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) { - documents = documents - .filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath))); - } - - if (documents.length > 0) { - const message = documents.length === 1 - ? l10n.t('The following file has unsaved changes which won\'t be included in the commit if you proceed: {0}.\n\nWould you like to save it before committing?', path.basename(documents[0].uri.fsPath)) - : l10n.t('There are {0} unsaved files.\n\nWould you like to save them before committing?', documents.length); - const saveAndCommit = l10n.t('Save All & Commit Changes'); - const commit = l10n.t('Commit Changes'); - const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit); - - if (pick === saveAndCommit) { - await Promise.all(documents.map(d => d.save())); - - // After saving the dirty documents, if there are any documents that are part of the - // index group we have to add them back in order for the saved changes to be committed + if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) { documents = documents .filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath))); - await repository.add(documents.map(d => d.uri)); + } - noStagedChanges = repository.indexGroup.resourceStates.length === 0; - noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; - } else if (pick !== commit) { - return; // do not commit on cancel + if (documents.length > 0) { + const message = documents.length === 1 + ? l10n.t('The following file has unsaved changes which won\'t be included in the commit if you proceed: {0}.\n\nWould you like to save it before committing?', path.basename(documents[0].uri.fsPath)) + : l10n.t('There are {0} unsaved files.\n\nWould you like to save them before committing?', documents.length); + const saveAndCommit = l10n.t('Save All & Commit Changes'); + const commit = l10n.t('Commit Changes'); + const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit); + + if (pick === saveAndCommit) { + await Promise.all(documents.map(d => d.save())); + + // After saving the dirty documents, if there are any documents that are part of the + // index group we have to add them back in order for the saved changes to be committed + documents = documents + .filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath))); + await repository.add(documents.map(d => d.uri)); + + noStagedChanges = repository.indexGroup.resourceStates.length === 0; + noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; + } else if (pick !== commit) { + return; // do not commit on cancel + } } } - } - // no changes, and the user has not configured to commit all in this case - if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.empty && !opts.all) { - const suggestSmartCommit = config.get('suggestSmartCommit') === true; + // no changes, and the user has not configured to commit all in this case + if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.all && !opts.amend) { + const suggestSmartCommit = config.get('suggestSmartCommit') === true; - if (!suggestSmartCommit) { - return; + if (!suggestSmartCommit) { + return; + } + + // prompt the user if we want to commit all or not + const message = l10n.t('There are no staged changes to commit.\n\nWould you like to stage all your changes and commit them directly?'); + const yes = l10n.t('Yes'); + const always = l10n.t('Always'); + const never = l10n.t('Never'); + const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never); + + if (pick === always) { + enableSmartCommit = true; + config.update('enableSmartCommit', true, true); + } else if (pick === never) { + config.update('suggestSmartCommit', false, true); + return; + } else if (pick === yes) { + enableSmartCommit = true; + } else { + // Cancel + return; + } } - // prompt the user if we want to commit all or not - const message = l10n.t('There are no staged changes to commit.\n\nWould you like to stage all your changes and commit them directly?'); - const yes = l10n.t('Yes'); - const always = l10n.t('Always'); - const never = l10n.t('Never'); - const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never); - - if (pick === always) { - config.update('enableSmartCommit', true, true); - } else if (pick === never) { - config.update('suggestSmartCommit', false, true); - return; - } else if (pick !== yes) { - return; // do not commit on cancel + // smart commit + if (enableSmartCommit && !opts.all) { + opts = { ...opts, all: noStagedChanges }; } } - if (opts.all === undefined) { - opts = { ...opts, all: noStagedChanges }; - } else if (!opts.all && noStagedChanges && !opts.empty) { - opts = { ...opts, all: true }; - } - // enable signing of commits if configured opts.signCommit = enableCommitSigning; @@ -2515,9 +2520,26 @@ export class CommandCenter { : l10n.t('Select a branch or tag to checkout'); quickPick.show(); - picks.push(... await createCheckoutItems(repository, opts?.detached)); - quickPick.items = [...commands, ...picks]; + + const setQuickPickItems = () => { + switch (true) { + case quickPick.value === '': + quickPick.items = [...commands, ...picks]; + break; + case commands.length === 0: + quickPick.items = picks; + break; + case picks.length === 0: + quickPick.items = commands; + break; + default: + quickPick.items = [...picks, { label: '', kind: QuickPickItemKind.Separator }, ...commands]; + break; + } + }; + + setQuickPickItems(); quickPick.busy = false; const choice = await new Promise(c => { @@ -2532,22 +2554,7 @@ export class CommandCenter { c(undefined); }))); - disposables.push(quickPick.onDidChangeValue(value => { - switch (true) { - case value === '': - quickPick.items = [...commands, ...picks]; - break; - case commands.length === 0: - quickPick.items = picks; - break; - case picks.length === 0: - quickPick.items = commands; - break; - default: - quickPick.items = [...picks, { label: '', kind: QuickPickItemKind.Separator }, ...commands]; - break; - } - })); + disposables.push(quickPick.onDidChangeValue(() => setQuickPickItems())); }); dispose(disposables); @@ -4400,10 +4407,10 @@ export class CommandCenter { private getSCMResource(uri?: Uri): Resource | undefined { uri = uri ? uri : (window.activeTextEditor && window.activeTextEditor.document.uri); - this.logger.debug(`git.getSCMResource.uri ${uri && uri.toString()}`); + this.logger.debug(`[CommandCenter][getSCMResource] git.getSCMResource.uri: ${uri && uri.toString()}`); for (const r of this.model.repositories.map(r => r.root)) { - this.logger.debug(`repo root ${r}`); + this.logger.debug(`[CommandCenter][getSCMResource] repo root: ${r}`); } if (!uri) { diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 3f8553260e9..ace68c22524 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -220,16 +220,16 @@ class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider const historyProvider = this.repository.historyProvider; const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; - if (!currentHistoryItemGroup?.base) { + if (!currentHistoryItemGroup?.remote) { return []; } - const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); + const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.remote.id); if (!ancestor) { return []; } - const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id); + const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.remote.id); return changes; } catch (err) { return []; diff --git a/extensions/git/src/encoding.ts b/extensions/git/src/encoding.ts index a283f628594..c80fb6ee6d5 100644 --- a/extensions/git/src/encoding.ts +++ b/extensions/git/src/encoding.ts @@ -49,15 +49,38 @@ const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { 'big5': 'cp950' }; -export function detectEncoding(buffer: Buffer): string | null { +const MAP_CANDIDATE_GUESS_ENCODING_TO_JSCHARDET: { [key: string]: string } = { + utf8: 'UTF-8', + utf16le: 'UTF-16LE', + utf16be: 'UTF-16BE', + windows1252: 'windows-1252', + windows1250: 'windows-1250', + iso88592: 'ISO-8859-2', + windows1251: 'windows-1251', + cp866: 'IBM866', + iso88595: 'ISO-8859-5', + koi8r: 'KOI8-R', + windows1253: 'windows-1253', + iso88597: 'ISO-8859-7', + windows1255: 'windows-1255', + iso88598: 'ISO-8859-8', + cp950: 'Big5', + shiftjis: 'SHIFT_JIS', + eucjp: 'EUC-JP', + euckr: 'EUC-KR', + gb2312: 'GB2312' +}; + +export function detectEncoding(buffer: Buffer, candidateGuessEncodings: string[]): string | null { const result = detectEncodingByBOM(buffer); if (result) { return result; } - const detected = jschardet.detect(buffer); + candidateGuessEncodings = candidateGuessEncodings.map(e => MAP_CANDIDATE_GUESS_ENCODING_TO_JSCHARDET[e]).filter(e => !!e); + const detected = jschardet.detect(buffer, candidateGuessEncodings.length > 0 ? { detectEncodings: candidateGuessEncodings } : undefined); if (!detected || !detected.encoding) { return null; } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 697e77815e4..eb0007893e8 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1113,7 +1113,7 @@ export class Repository { return result.stdout.trim(); } catch (err) { - this.logger.warn(`git config failed: ${err.message}`); + this.logger.warn(`[Git][config] git config failed: ${err.message}`); return ''; } } @@ -1165,6 +1165,12 @@ export class Repository { args.push(`--author="${options.author}"`); } + if (options?.refNames) { + args.push('--topo-order'); + args.push('--decorate=full'); + args.push(...options.refNames); + } + if (options?.path) { args.push('--', options.path); } @@ -1233,11 +1239,11 @@ export class Repository { .filter(entry => !!entry); } - async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise { + async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false, candidateGuessEncodings: string[] = []): Promise { const stdout = await this.buffer(object); if (autoGuessEncoding) { - encoding = detectEncoding(stdout) || encoding; + encoding = detectEncoding(stdout, candidateGuessEncodings) || encoding; } encoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; @@ -1496,9 +1502,16 @@ export class Repository { return parseGitChanges(this.repositoryRoot, gitResult.stdout); } - async getMergeBase(ref1: string, ref2: string): Promise { + async getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise { try { - const args = ['merge-base', ref1, ref2]; + const args = ['merge-base']; + if (refs.length !== 0) { + args.push('--octopus'); + args.push(...refs); + } + + args.push(ref1, ref2); + const result = await this.exec(args); return result.stdout.trim(); @@ -2304,7 +2317,7 @@ export class Repository { return result; } catch (err) { - this.logger.warn(err.message); + this.logger.warn(`[Git][getHEAD] Failed to parse HEAD file: ${err.message}`); } try { @@ -2452,11 +2465,11 @@ export class Repository { remotes.push(...await this.getRemotesFS()); if (remotes.length === 0) { - this.logger.info('No remotes found in the git config file.'); + this.logger.info('[Git][getRemotes] No remotes found in the git config file'); } } catch (err) { - this.logger.warn(`getRemotes() - ${err.message}`); + this.logger.warn(`[Git][getRemotes] Error: ${err.message}`); // Fallback to using git to get the remotes remotes.push(...await this.getRemotesGit()); @@ -2592,7 +2605,7 @@ export class Repository { return branch; } - this.logger.warn(`No such branch: ${name}.`); + this.logger.warn(`[Git][getBranch] No such branch: ${name}`); return Promise.reject(new Error(`No such branch: ${name}.`)); } @@ -2688,7 +2701,7 @@ export class Repository { const result = await fs.readFile(path.join(this.dotGit.path, ref), 'utf8'); return result.trim(); } catch (err) { - this.logger.warn(err.message); + this.logger.warn(`[Git][revParse] Unable to read file: ${err.message}`); } try { diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index f238010e14c..8bd8b70022d 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel } from 'vscode'; +import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemLabel } from 'vscode'; import { Repository, Resource } from './repository'; import { IDisposable, dispose, filterEvent } from './util'; import { toGitUri } from './uri'; import { Branch, RefType, UpstreamRef } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Operation } from './operation'; +import { Commit } from './git'; export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable { @@ -21,6 +22,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; private _HEAD: Branch | undefined; + private _HEADMergeBase: Branch | undefined; + private _currentHistoryItemGroup: SourceControlHistoryItemGroup | undefined; get currentHistoryItemGroup(): SourceControlHistoryItemGroup | undefined { return this._currentHistoryItemGroup; } set currentHistoryItemGroup(value: SourceControlHistoryItemGroup | undefined) { @@ -29,6 +32,12 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } private historyItemDecorations = new Map(); + private historyItemLabels = new Map([ + ['HEAD -> refs/heads/', 'target'], + ['refs/heads/', 'git-branch'], + ['refs/remotes/', 'cloud'], + ['refs/tags/', 'tag'] + ]); private disposables: Disposable[] = []; @@ -40,8 +49,11 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } private async onDidRunGitStatus(force = false): Promise { - this.logger.trace('GitHistoryProvider:onDidRunGitStatus - HEAD:', JSON.stringify(this._HEAD)); - this.logger.trace('GitHistoryProvider:onDidRunGitStatus - repository.HEAD:', JSON.stringify(this.repository.HEAD)); + this.logger.trace('[GitHistoryProvider][onDidRunGitStatus] HEAD:', JSON.stringify(this._HEAD)); + this.logger.trace('[GitHistoryProvider][onDidRunGitStatus] repository.HEAD:', JSON.stringify(this.repository.HEAD)); + + // Get the merge base of the current history item group + const mergeBase = await this.resolveHEADMergeBase(); // Check if HEAD has changed if (!force && @@ -49,16 +61,20 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this._HEAD?.commit === this.repository.HEAD?.commit && this._HEAD?.upstream?.name === this.repository.HEAD?.upstream?.name && this._HEAD?.upstream?.remote === this.repository.HEAD?.upstream?.remote && - this._HEAD?.upstream?.commit === this.repository.HEAD?.upstream?.commit) { - this.logger.trace('GitHistoryProvider:onDidRunGitStatus - HEAD has not changed'); + this._HEAD?.upstream?.commit === this.repository.HEAD?.upstream?.commit && + this._HEADMergeBase?.name === mergeBase?.name && + this._HEADMergeBase?.remote === mergeBase?.remote && + this._HEADMergeBase?.commit === mergeBase?.commit) { + this.logger.trace('[GitHistoryProvider][onDidRunGitStatus] HEAD has not changed'); return; } this._HEAD = this.repository.HEAD; + this._HEADMergeBase = mergeBase; // Check if HEAD does not support incoming/outgoing (detached commit, tag) if (!this.repository.HEAD?.name || !this.repository.HEAD?.commit || this.repository.HEAD.type === RefType.Tag) { - this.logger.trace('GitHistoryProvider:onDidRunGitStatus - HEAD does not support incoming/outgoing'); + this.logger.trace('[GitHistoryProvider][onDidRunGitStatus] HEAD does not support incoming/outgoing'); this.currentHistoryItemGroup = undefined; return; @@ -67,14 +83,17 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.currentHistoryItemGroup = { id: `refs/heads/${this.repository.HEAD.name ?? ''}`, name: this.repository.HEAD.name ?? '', - base: this.repository.HEAD.upstream ? - { - id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - } : undefined + remote: this.repository.HEAD.upstream ? { + id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + } : undefined, + base: mergeBase ? { + id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, + name: `${mergeBase.remote}/${mergeBase.name}`, + } : undefined }; - this.logger.trace(`GitHistoryProvider:onDidRunGitStatus - currentHistoryItemGroup (${force}): ${JSON.stringify(this.currentHistoryItemGroup)}`); + this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemGroup(${force}): ${JSON.stringify(this.currentHistoryItemGroup)}`); } async provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions): Promise { @@ -112,6 +131,47 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return historyItems; } + async provideHistoryItems2(options: SourceControlHistoryOptions): Promise { + if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) { + return []; + } + + // Deduplicate refNames + const refNames = Array.from(new Set(options.historyItemGroupIds)); + + // Get the merge base of the refNames + const refsMergeBase = await this.resolveHistoryItemGroupsMergeBase(refNames); + if (!refsMergeBase) { + return []; + } + + // Get the commits + const commits = await this.repository.log({ range: `${refsMergeBase}^..`, refNames, shortStats: true }); + + await ensureEmojis(); + + const historyItems: SourceControlHistoryItem[] = []; + historyItems.push(...commits.map(commit => { + const newLineIndex = commit.message.indexOf('\n'); + const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; + + const labels = this.resolveHistoryItemLabels(commit, refNames); + + return { + id: commit.hash, + parentIds: commit.parents, + message: emojify(subject), + author: commit.authorName, + icon: new ThemeIcon('git-commit'), + timestamp: commit.authorDate?.getTime(), + statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, + labels: labels.length !== 0 ? labels : undefined + }; + })); + + return historyItems; + } + async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { if (!historyItemParentId) { const commit = await this.repository.getCommit(historyItemId); @@ -161,9 +221,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec async resolveHistoryItemGroupCommonAncestor(historyItemId1: string, historyItemId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> { if (!historyItemId2) { - const upstreamRef = await this.resolveHistoryItemGroupBase(historyItemId1); + const upstreamRef = await this.resolveHistoryItemGroupMergeBase(historyItemId1); if (!upstreamRef) { - this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupCommonAncestor - Failed to resolve history item group base for '${historyItemId1}'`); + this.logger.info(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor] Failed to resolve history item group base for '${historyItemId1}'`); return undefined; } @@ -172,16 +232,16 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec const ancestor = await this.repository.getMergeBase(historyItemId1, historyItemId2); if (!ancestor) { - this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupCommonAncestor - Failed to resolve common ancestor for '${historyItemId1}' and '${historyItemId2}'`); + this.logger.info(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor] Failed to resolve common ancestor for '${historyItemId1}' and '${historyItemId2}'`); return undefined; } try { const commitCount = await this.repository.getCommitCount(`${historyItemId1}...${historyItemId2}`); - this.logger.trace(`GitHistoryProvider:resolveHistoryItemGroupCommonAncestor - Resolved common ancestor for '${historyItemId1}' and '${historyItemId2}': ${JSON.stringify({ id: ancestor, ahead: commitCount.ahead, behind: commitCount.behind })}`); + this.logger.trace(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor] Resolved common ancestor for '${historyItemId1}' and '${historyItemId2}': ${JSON.stringify({ id: ancestor, ahead: commitCount.ahead, behind: commitCount.behind })}`); return { id: ancestor, ahead: commitCount.ahead, behind: commitCount.behind }; } catch (err) { - this.logger.error(`GitHistoryProvider:resolveHistoryItemGroupCommonAncestor - Failed to get ahead/behind for '${historyItemId1}...${historyItemId2}': ${err.message}`); + this.logger.error(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor] Failed to get ahead/behind for '${historyItemId1}...${historyItemId2}': ${err.message}`); } return undefined; @@ -191,7 +251,38 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return this.historyItemDecorations.get(uri.toString()); } - private async resolveHistoryItemGroupBase(historyItemId: string): Promise { + private async resolveHistoryItemGroupsMergeBase(refNames: string[]): Promise { + if (refNames.length < 2) { + return undefined; + } + + const refsMergeBase = await this.repository.getMergeBase(refNames[0], refNames[1], ...refNames.slice(2)); + return refsMergeBase; + } + + private resolveHistoryItemLabels(commit: Commit, refNames: string[]): SourceControlHistoryItemLabel[] { + const labels: SourceControlHistoryItemLabel[] = []; + + for (const label of commit.refNames) { + if (!label.startsWith('HEAD -> ') && !refNames.includes(label)) { + continue; + } + + for (const [key, value] of this.historyItemLabels) { + if (label.startsWith(key)) { + labels.push({ + title: label.substring(key.length), + icon: new ThemeIcon(value) + }); + break; + } + } + } + + return labels; + } + + private async resolveHistoryItemGroupMergeBase(historyItemId: string): Promise { try { // Upstream const branch = await this.repository.getBranch(historyItemId); @@ -202,7 +293,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // Base (config -> reflog -> default) const remoteBranch = await this.repository.getBranchBase(historyItemId); if (!remoteBranch?.remote || !remoteBranch?.name || !remoteBranch?.commit || remoteBranch?.type !== RefType.RemoteHead) { - this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to resolve history item group base for '${historyItemId}'`); + this.logger.info(`[GitHistoryProvider][resolveHistoryItemGroupUpstreamOrBase] Failed to resolve history item group base for '${historyItemId}'`); return undefined; } @@ -213,12 +304,21 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec }; } catch (err) { - this.logger.error(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to get branch base for '${historyItemId}': ${err.message}`); + this.logger.error(`[GitHistoryProvider][resolveHistoryItemGroupUpstreamOrBase] Failed to get branch base for '${historyItemId}': ${err.message}`); } return undefined; } + private async resolveHEADMergeBase(): Promise { + if (this.repository.HEAD?.type !== RefType.Head || !this.repository.HEAD?.name) { + return undefined; + } + + const mergeBase = await this.repository.getBranchBase(this.repository.HEAD.name); + return mergeBase; + } + dispose(): void { dispose(this.disposables); } diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index c2d9b974be7..b3f6b6466fe 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -48,7 +48,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, } const info = await findGit(pathHints, gitPath => { - logger.info(l10n.t('Validating found git in: "{0}"', gitPath)); + logger.info(l10n.t('[main] Validating found git in: "{0}"', gitPath)); if (excludes.length === 0) { return true; } @@ -56,7 +56,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, const normalized = path.normalize(gitPath).replace(/[\r\n]+$/, ''); const skip = excludes.some(e => normalized.startsWith(e)); if (skip) { - logger.info(l10n.t('Skipped found git in: "{0}"', gitPath)); + logger.info(l10n.t('[main] Skipped found git in: "{0}"', gitPath)); } return !skip; }, logger); @@ -66,7 +66,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, try { ipcServer = await createIPCServer(context.storagePath); } catch (err) { - logger.error(`Failed to create git IPC: ${err}`); + logger.error(`[main] Failed to create git IPC: ${err}`); } const askpass = new Askpass(ipcServer); @@ -79,7 +79,7 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, const terminalEnvironmentManager = new TerminalEnvironmentManager(context, [askpass, gitEditor, ipcServer]); disposables.push(terminalEnvironmentManager); - logger.info(l10n.t('Using git "{0}" from "{1}"', info.version, info.path)); + logger.info(l10n.t('[main] Using git "{0}" from "{1}"', info.version, info.path)); const git = new Git({ gitPath: info.path, @@ -187,7 +187,7 @@ export async function _activate(context: ExtensionContext): Promise { - logger.appendLine(l10n.t('Log level: {0}', LogLevel[logLevel])); + logger.appendLine(l10n.t('[main] Log level: {0}', LogLevel[logLevel])); }; disposables.push(logger.onDidChangeLogLevel(onDidChangeLogLevel)); onDidChangeLogLevel(logger.logLevel); @@ -212,13 +212,13 @@ export async function _activate(context: ExtensionContext): Promise { + this.logger.info('[Model][doInitialScan] Initial repository scan started'); + const config = workspace.getConfiguration('git'); const autoRepositoryDetection = config.get('autoRepositoryDetection'); const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt'); + this.logger.trace(`[Model][doInitialScan] Settings: autoRepositoryDetection=${autoRepositoryDetection}, openRepositoryInParentFolders=${parentRepositoryConfig}`); + // Initial repository scan function const initialScanFn = () => Promise.all([ this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] }), @@ -321,6 +325,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } */ this.telemetryReporter.sendTelemetryEvent('git.repositoryInitialScan', { autoRepositoryDetection: String(autoRepositoryDetection) }, { repositoryCount: this.openRepositories.length }); + this.logger.info(`[Model][doInitialScan] Initial repository scan completed - repositories(${this.repositories.length}), closed repositories (${this.closedRepositories.length}), parent repositories (${this.parentRepositories.length}), unsafe repositories (${this.unsafeRepositories.length})`); } /** @@ -329,47 +334,51 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi * the git.repositoryScanMaxDepth setting. */ private async scanWorkspaceFolders(): Promise { - const config = workspace.getConfiguration('git'); - const autoRepositoryDetection = config.get('autoRepositoryDetection'); - this.logger.trace(`[swsf] Scan workspace sub folders. autoRepositoryDetection=${autoRepositoryDetection}`); + try { + const config = workspace.getConfiguration('git'); + const autoRepositoryDetection = config.get('autoRepositoryDetection'); - if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') { - return; - } - - await Promise.all((workspace.workspaceFolders || []).map(async folder => { - const root = folder.uri.fsPath; - this.logger.trace(`[swsf] Workspace folder: ${root}`); - - // Workspace folder children - const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('repositoryScanMaxDepth', 1); - const repositoryScanIgnoredFolders = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('repositoryScanIgnoredFolders', []); - - const subfolders = new Set(await this.traverseWorkspaceFolder(root, repositoryScanMaxDepth, repositoryScanIgnoredFolders)); - - // Repository scan folders - const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('scanRepositories') || []; - this.logger.trace(`[swsf] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`); - - for (const scanPath of scanPaths) { - if (scanPath === '.git') { - this.logger.trace('[swsf] \'.git\' not supported in \'git.scanRepositories\' setting.'); - continue; - } - - if (path.isAbsolute(scanPath)) { - const notSupportedMessage = l10n.t('Absolute paths not supported in "git.scanRepositories" setting.'); - this.logger.warn(notSupportedMessage); - console.warn(notSupportedMessage); - continue; - } - - subfolders.add(path.join(root, scanPath)); + if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') { + return; } - this.logger.trace(`[swsf] Workspace scan sub folders: [${[...subfolders].join(', ')}]`); - await Promise.all([...subfolders].map(f => this.openRepository(f))); - })); + await Promise.all((workspace.workspaceFolders || []).map(async folder => { + const root = folder.uri.fsPath; + this.logger.trace(`[Model][scanWorkspaceFolders] Workspace folder: ${root}`); + + // Workspace folder children + const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('repositoryScanMaxDepth', 1); + const repositoryScanIgnoredFolders = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('repositoryScanIgnoredFolders', []); + + const subfolders = new Set(await this.traverseWorkspaceFolder(root, repositoryScanMaxDepth, repositoryScanIgnoredFolders)); + + // Repository scan folders + const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('scanRepositories') || []; + this.logger.trace(`[Model][scanWorkspaceFolders] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`); + + for (const scanPath of scanPaths) { + if (scanPath === '.git') { + this.logger.trace('[Model][scanWorkspaceFolders] \'.git\' not supported in \'git.scanRepositories\' setting.'); + continue; + } + + if (path.isAbsolute(scanPath)) { + const notSupportedMessage = l10n.t('Absolute paths not supported in "git.scanRepositories" setting.'); + this.logger.warn(`[Model][scanWorkspaceFolders] ${notSupportedMessage}`); + console.warn(notSupportedMessage); + continue; + } + + subfolders.add(path.join(root, scanPath)); + } + + this.logger.trace(`[Model][scanWorkspaceFolders] Workspace scan sub folders: [${[...subfolders].join(', ')}]`); + await Promise.all([...subfolders].map(f => this.openRepository(f))); + })); + } + catch (err) { + this.logger.warn(`[Model][scanWorkspaceFolders] Error: ${err}`); + } } private async traverseWorkspaceFolder(workspaceFolder: string, maxDepth: number, repositoryScanIgnoredFolders: string[]): Promise { @@ -379,15 +388,26 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi while (foldersToTravers.length > 0) { const currentFolder = foldersToTravers.shift()!; + const children: fs.Dirent[] = []; + try { + children.push(...await fs.promises.readdir(currentFolder.path, { withFileTypes: true })); + + if (currentFolder.depth !== 0) { + result.push(currentFolder.path); + } + } + catch (err) { + this.logger.warn(`[Model][traverseWorkspaceFolder] Unable to read workspace folder '${currentFolder.path}': ${err}`); + continue; + } + if (currentFolder.depth < maxDepth || maxDepth === -1) { - const children = await fs.promises.readdir(currentFolder.path, { withFileTypes: true }); const childrenFolders = children .filter(dirent => dirent.isDirectory() && dirent.name !== '.git' && !repositoryScanIgnoredFolders.find(f => pathEquals(dirent.name, f))) .map(dirent => path.join(currentFolder.path, dirent.name)); - result.push(...childrenFolders); foldersToTravers.push(...childrenFolders.map(folder => { return { path: folder, depth: currentFolder.depth + 1 }; })); @@ -423,23 +443,28 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } private async onDidChangeWorkspaceFolders({ added, removed }: WorkspaceFoldersChangeEvent): Promise { - const possibleRepositoryFolders = added - .filter(folder => !this.getOpenRepository(folder.uri)); + try { + const possibleRepositoryFolders = added + .filter(folder => !this.getOpenRepository(folder.uri)); - const activeRepositoriesList = window.visibleTextEditors - .map(editor => this.getRepository(editor.document.uri)) - .filter(repository => !!repository) as Repository[]; + const activeRepositoriesList = window.visibleTextEditors + .map(editor => this.getRepository(editor.document.uri)) + .filter(repository => !!repository) as Repository[]; - const activeRepositories = new Set(activeRepositoriesList); - const openRepositoriesToDispose = removed - .map(folder => this.getOpenRepository(folder.uri)) - .filter(r => !!r) - .filter(r => !activeRepositories.has(r!.repository)) - .filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[]; + const activeRepositories = new Set(activeRepositoriesList); + const openRepositoriesToDispose = removed + .map(folder => this.getOpenRepository(folder.uri)) + .filter(r => !!r) + .filter(r => !activeRepositories.has(r!.repository)) + .filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[]; - openRepositoriesToDispose.forEach(r => r.dispose()); - this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`); - await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath))); + openRepositoriesToDispose.forEach(r => r.dispose()); + this.logger.trace(`[Model][onDidChangeWorkspaceFolders] Workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`); + await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath))); + } + catch (err) { + this.logger.warn(`[Model][onDidChangeWorkspaceFolders] Error: ${err}`); + } } private onDidChangeConfiguration(): void { @@ -452,50 +477,54 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi .filter(({ root }) => workspace.getConfiguration('git', root).get('enabled') !== true) .map(({ repository }) => repository); - this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`); + this.logger.trace(`[Model][onDidChangeConfiguration] Workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`); possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath)); openRepositoriesToDispose.forEach(r => r.dispose()); } private async onDidChangeVisibleTextEditors(editors: readonly TextEditor[]): Promise { - if (!workspace.isTrusted) { - this.logger.trace('[svte] Workspace is not trusted.'); - return; - } - - const config = workspace.getConfiguration('git'); - const autoRepositoryDetection = config.get('autoRepositoryDetection'); - this.logger.trace(`[svte] Scan visible text editors. autoRepositoryDetection=${autoRepositoryDetection}`); - - if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') { - return; - } - - await Promise.all(editors.map(async editor => { - const uri = editor.document.uri; - - if (uri.scheme !== 'file') { + try { + if (!workspace.isTrusted) { + this.logger.trace('[Model][onDidChangeVisibleTextEditors] Workspace is not trusted.'); return; } - const repository = this.getRepository(uri); + const config = workspace.getConfiguration('git'); + const autoRepositoryDetection = config.get('autoRepositoryDetection'); - if (repository) { - this.logger.trace(`[svte] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`); + if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') { return; } - this.logger.trace(`[svte] Open repository for editor resource ${uri.fsPath}`); - await this.openRepository(path.dirname(uri.fsPath)); - })); + await Promise.all(editors.map(async editor => { + const uri = editor.document.uri; + + if (uri.scheme !== 'file') { + return; + } + + const repository = this.getRepository(uri); + + if (repository) { + this.logger.trace(`[Model][onDidChangeVisibleTextEditors] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`); + return; + } + + this.logger.trace(`[Model][onDidChangeVisibleTextEditors] Open repository for editor resource ${uri.fsPath}`); + await this.openRepository(path.dirname(uri.fsPath)); + })); + } + catch (err) { + this.logger.warn(`[Model][onDidChangeVisibleTextEditors] Error: ${err}`); + } } @sequentialize async openRepository(repoPath: string, openIfClosed = false): Promise { - this.logger.trace(`Opening repository: ${repoPath}`); + this.logger.trace(`[Model][openRepository] Repository: ${repoPath}`); const existingRepository = await this.getRepositoryExact(repoPath); if (existingRepository) { - this.logger.trace(`Repository for path ${repoPath} already exists: ${existingRepository.root}`); + this.logger.trace(`[Model][openRepository] Repository for path ${repoPath} already exists: ${existingRepository.root}`); return; } @@ -503,7 +532,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi const enabled = config.get('enabled') === true; if (!enabled) { - this.logger.trace('Git is not enabled'); + this.logger.trace('[Model][openRepository] Git is not enabled'); return; } @@ -513,7 +542,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi fs.accessSync(path.join(repoPath, 'HEAD'), fs.constants.F_OK); const result = await this.git.exec(repoPath, ['-C', repoPath, 'rev-parse', '--show-cdup']); if (result.stderr.trim() === '' && result.stdout.trim() === '') { - this.logger.trace(`Bare repository: ${repoPath}`); + this.logger.trace(`[Model][openRepository] Bare repository: ${repoPath}`); return; } } catch { @@ -523,16 +552,16 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi try { const { repositoryRoot, unsafeRepositoryMatch } = await this.getRepositoryRoot(repoPath); - this.logger.trace(`Repository root for path ${repoPath} is: ${repositoryRoot}`); + this.logger.trace(`[Model][openRepository] Repository root for path ${repoPath} is: ${repositoryRoot}`); const existingRepository = await this.getRepositoryExact(repositoryRoot); if (existingRepository) { - this.logger.trace(`Repository for path ${repositoryRoot} already exists: ${existingRepository.root}`); + this.logger.trace(`[Model][openRepository] Repository for path ${repositoryRoot} already exists: ${existingRepository.root}`); return; } if (this.shouldRepositoryBeIgnored(repositoryRoot)) { - this.logger.trace(`Repository for path ${repositoryRoot} is ignored`); + this.logger.trace(`[Model][openRepository] Repository for path ${repositoryRoot} is ignored`); return; } @@ -541,7 +570,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi if (parentRepositoryConfig !== 'always' && this.globalState.get(`parentRepository:${repositoryRoot}`) !== true) { const isRepositoryOutsideWorkspace = await this.isRepositoryOutsideWorkspace(repositoryRoot); if (isRepositoryOutsideWorkspace) { - this.logger.trace(`Repository in parent folder: ${repositoryRoot}`); + this.logger.trace(`[Model][openRepository] Repository in parent folder: ${repositoryRoot}`); if (!this._parentRepositoriesManager.hasRepository(repositoryRoot)) { // Show a notification if the parent repository is opened after the initial scan @@ -558,7 +587,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi // Handle unsafe repositories if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) { - this.logger.trace(`Unsafe repository: ${repositoryRoot}`); + this.logger.trace(`[Model][openRepository] Unsafe repository: ${repositoryRoot}`); // Show a notification if the unsafe repository is opened after the initial scan if (this._state === 'initialized' && !this._unsafeRepositoriesManager.hasRepository(repositoryRoot)) { @@ -572,7 +601,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi // Handle repositories that were closed by the user if (!openIfClosed && this._closedRepositoriesManager.isRepositoryClosed(repositoryRoot)) { - this.logger.trace(`Repository for path ${repositoryRoot} is closed`); + this.logger.trace(`[Model][openRepository] Repository for path ${repositoryRoot} is closed`); return; } @@ -583,12 +612,14 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi this.open(repository); this._closedRepositoriesManager.deleteRepository(repository.root); + this.logger.info(`[Model][openRepository] Opened repository: ${repository.root}`); + // Do not await this, we want SCM // to know about the repo asap repository.status(); } catch (err) { // noop - this.logger.trace(`Opening repository for path='${repoPath}' failed; ex=${err}`); + this.logger.trace(`[Model][openRepository] Opening repository for path='${repoPath}' failed. Error:${err}`); } } @@ -620,7 +651,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi const repositoryRootRealPath = await fs.promises.realpath(repositoryRoot); return !pathEquals(repositoryRoot, repositoryRootRealPath) ? repositoryRootRealPath : undefined; } catch (err) { - this.logger.warn(`Failed to get repository realpath for: "${repositoryRoot}". ${err}`); + this.logger.warn(`[Model][getRepositoryRootRealPath] Failed to get repository realpath for "${repositoryRoot}": ${err}`); return undefined; } } @@ -647,7 +678,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } private open(repository: Repository): void { - this.logger.info(`Open repository: ${repository.root}`); + this.logger.trace(`[Model][open] Repository: ${repository.root}`); const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed); const disappearListener = onDidDisappearRepository(() => dispose()); @@ -664,7 +695,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi const checkForSubmodules = () => { if (!shouldDetectSubmodules) { - this.logger.trace('Automatic detection of git submodules is not enabled.'); + this.logger.trace('[Model][open] Automatic detection of git submodules is not enabled.'); return; } @@ -677,7 +708,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi .slice(0, submodulesLimit) .map(r => path.join(repository.root, r.path)) .forEach(p => { - this.logger.trace(`Opening submodule: '${p}'`); + this.logger.trace(`[Model][open] Opening submodule: '${p}'`); this.eventuallyScanPossibleGitRepository(p); }); }; @@ -739,7 +770,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return; } - this.logger.info(`Close repository: ${repository.root}`); + this.logger.info(`[Model][close] Repository: ${repository.root}`); this._closedRepositoriesManager.addRepository(openRepository.repository.root); openRepository.dispose(); @@ -792,7 +823,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return openRepositoryRealPath?.repository; } catch (err) { - this.logger.warn(`Failed to get repository realpath for: "${repoPath}". ${err}`); + this.logger.warn(`[Model][getRepositoryExact] Failed to get repository realpath for: "${repoPath}". Error:${err}`); return undefined; } } @@ -978,7 +1009,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi this._workspaceFolders.set(workspaceFolder.uri.fsPath, result); } catch (err) { // noop - Workspace folder does not exist - this.logger.trace(`Failed to resolve workspace folder: "${workspaceFolder.uri.fsPath}". ${err}`); + this.logger.trace(`[Model][getWorkspaceFolderRealPath] Failed to resolve workspace folder "${workspaceFolder.uri.fsPath}". Error:${err}`); } } diff --git a/extensions/git/src/operation.ts b/extensions/git/src/operation.ts index 223f1945b02..13370d59bf7 100644 --- a/extensions/git/src/operation.ts +++ b/extensions/git/src/operation.ts @@ -141,7 +141,7 @@ export const Operation = { CheckoutTracking: (refLabel: string) => ({ kind: OperationKind.CheckoutTracking, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true, refLabel } as CheckoutTrackingOperation), Clean: (showProgress: boolean) => ({ kind: OperationKind.Clean, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as CleanOperation), Commit: { kind: OperationKind.Commit, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true } as CommitOperation, - Config: (readOnly: boolean) => ({ kind: OperationKind.Config, blocking: false, readOnly, remote: false, retry: false, showProgress: true } as ConfigOperation), + Config: (readOnly: boolean) => ({ kind: OperationKind.Config, blocking: false, readOnly, remote: false, retry: false, showProgress: false } as ConfigOperation), DeleteBranch: { kind: OperationKind.DeleteBranch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteBranchOperation, DeleteRef: { kind: OperationKind.DeleteRef, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteRefOperation, DeleteRemoteTag: { kind: OperationKind.DeleteRemoteTag, blocking: false, readOnly: false, remote: true, retry: false, showProgress: true } as DeleteRemoteTagOperation, @@ -149,7 +149,7 @@ export const Operation = { Diff: { kind: OperationKind.Diff, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as DiffOperation, Fetch: (showProgress: boolean) => ({ kind: OperationKind.Fetch, blocking: false, readOnly: false, remote: true, retry: true, showProgress } as FetchOperation), FindTrackingBranches: { kind: OperationKind.FindTrackingBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as FindTrackingBranchesOperation, - GetBranch: { kind: OperationKind.GetBranch, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchOperation, + GetBranch: { kind: OperationKind.GetBranch, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetBranchOperation, GetBranches: { kind: OperationKind.GetBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchesOperation, GetCommitTemplate: { kind: OperationKind.GetCommitTemplate, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetCommitTemplateOperation, GetObjectDetails: { kind: OperationKind.GetObjectDetails, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectDetailsOperation, @@ -214,7 +214,7 @@ export class OperationManager implements IOperationManager { this.operations.set(operation.kind, new Set([operation])); } - this.logger.trace(`Operation start: ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`); + this.logger.trace(`[OperationManager][start] ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`); } end(operation: Operation): void { @@ -226,7 +226,7 @@ export class OperationManager implements IOperationManager { } } - this.logger.trace(`Operation end: ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`); + this.logger.trace(`[OperationManager][end] ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`); } getOperations(operationKind: OperationKind): Operation[] { diff --git a/extensions/git/src/protocolHandler.ts b/extensions/git/src/protocolHandler.ts index dc73fe39965..90491fecd50 100644 --- a/extensions/git/src/protocolHandler.ts +++ b/extensions/git/src/protocolHandler.ts @@ -22,7 +22,7 @@ export class GitProtocolHandler implements UriHandler { } handleUri(uri: Uri): void { - this.logger.info(`GitProtocolHandler.handleUri(${uri.toString()})`); + this.logger.info(`[GitProtocolHandler][handleUri] URI:(${uri.toString()})`); switch (uri.path) { case '/clone': this.clone(uri); @@ -34,17 +34,17 @@ export class GitProtocolHandler implements UriHandler { const ref = data.ref; if (!data.url) { - this.logger.warn('Failed to open URI:' + uri.toString()); + this.logger.warn('[GitProtocolHandler][clone] Failed to open URI:' + uri.toString()); return; } if (Array.isArray(data.url) && data.url.length === 0) { - this.logger.warn('Failed to open URI:' + uri.toString()); + this.logger.warn('[GitProtocolHandler][clone] Failed to open URI:' + uri.toString()); return; } if (ref !== undefined && typeof ref !== 'string') { - this.logger.warn('Failed to open URI due to multiple references:' + uri.toString()); + this.logger.warn('[GitProtocolHandler][clone] Failed to open URI due to multiple references:' + uri.toString()); return; } @@ -69,12 +69,12 @@ export class GitProtocolHandler implements UriHandler { } } catch (ex) { - this.logger.warn('Invalid URI:' + uri.toString()); + this.logger.warn('[GitProtocolHandler][clone] Invalid URI:' + uri.toString()); return; } if (!(await commands.getCommands(true)).includes('git.clone')) { - this.logger.error('Could not complete git clone operation as git installation was not found.'); + this.logger.error('[GitProtocolHandler][clone] Could not complete git clone operation as git installation was not found.'); const errorMessage = l10n.t('Could not clone your repository as Git is not installed.'); const downloadGit = l10n.t('Download Git'); @@ -86,7 +86,7 @@ export class GitProtocolHandler implements UriHandler { return; } else { const cloneTarget = cloneUri.toString(true); - this.logger.info(`Executing git.clone for ${cloneTarget}`); + this.logger.info(`[GitProtocolHandler][clone] Executing git.clone for ${cloneTarget}`); commands.executeCommand('git.clone', cloneTarget, undefined, { ref: ref }); } } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index ed959765a59..5c14345e61a 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -426,8 +426,8 @@ class FileEventLogger { } this.eventDisposable = combinedDisposable([ - this.onWorkspaceWorkingTreeFileChange(uri => this.logger.debug(`[wt] Change: ${uri.fsPath}`)), - this.onDotGitFileChange(uri => this.logger.debug(`[.git] Change: ${uri.fsPath}`)) + this.onWorkspaceWorkingTreeFileChange(uri => this.logger.debug(`[FileEventLogger][onWorkspaceWorkingTreeFileChange] ${uri.fsPath}`)), + this.onDotGitFileChange(uri => this.logger.debug(`[FileEventLogger][onDotGitFileChange] ${uri.fsPath}`)) ]); } @@ -478,7 +478,7 @@ class DotGitWatcher implements IFileWatcher { this.transientDisposables.push(upstreamWatcher); upstreamWatcher.event(this.emitter.fire, this.emitter, this.transientDisposables); } catch (err) { - this.logger.warn(`Failed to watch ref '${upstreamPath}', is most likely packed.`); + this.logger.warn(`[DotGitWatcher][updateTransientWatchers] Failed to watch ref '${upstreamPath}', is most likely packed.`); } } @@ -1112,8 +1112,8 @@ export class Repository implements Disposable { return this.run(Operation.Diff, () => this.repository.diffBetweenShortStat(ref1, ref2)); } - getMergeBase(ref1: string, ref2: string): Promise { - return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2)); + getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise { + return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2, ...refs)); } async hashObject(data: string): Promise { @@ -1523,7 +1523,7 @@ export class Repository implements Disposable { return upstreamBranch; } catch (err) { - this.logger.warn(`Failed to get branch details for 'refs/remotes/${branch.upstream.remote}/${branch.upstream.name}': ${err.message}.`); + this.logger.warn(`[Repository][getUpstreamBranch] Failed to get branch details for 'refs/remotes/${branch.upstream.remote}/${branch.upstream.name}': ${err.message}.`); return undefined; } } @@ -1865,13 +1865,14 @@ export class Repository implements Disposable { const configFiles = workspace.getConfiguration('files', Uri.file(filePath)); const defaultEncoding = configFiles.get('encoding'); const autoGuessEncoding = configFiles.get('autoGuessEncoding'); + const candidateGuessEncodings = configFiles.get('candidateGuessEncodings'); try { - return await this.repository.bufferString(`${ref}:${path}`, defaultEncoding, autoGuessEncoding); + return await this.repository.bufferString(`${ref}:${path}`, defaultEncoding, autoGuessEncoding, candidateGuessEncodings); } catch (err) { if (err.gitErrorCode === GitErrorCodes.WrongCase) { const gitRelativePath = await this.repository.getGitRelativePath(ref, path); - return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding); + return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding, candidateGuessEncodings); } throw err; @@ -2408,17 +2409,17 @@ export class Repository implements Disposable { const autorefresh = config.get('autorefresh'); if (!autorefresh) { - this.logger.trace('Skip running git status because autorefresh setting is disabled.'); + this.logger.trace('[Repository][onFileChange] Skip running git status because autorefresh setting is disabled.'); return; } if (this.isRepositoryHuge) { - this.logger.trace('Skip running git status because repository is huge.'); + this.logger.trace('[Repository][onFileChange] Skip running git status because repository is huge.'); return; } if (!this.operations.isIdle()) { - this.logger.trace('Skip running git status because an operation is running.'); + this.logger.trace('[Repository][onFileChange] Skip running git status because an operation is running.'); return; } diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index 266157e9e5c..a7e1a693f84 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -182,10 +182,10 @@ isexe@^3.1.1: resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== -jschardet@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" - integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +jschardet@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.1.3.tgz#10c2289fdae91a0aa9de8bba9c59055fd78898d3" + integrity sha512-Q1PKVMK/uu+yjdlobgWIYkUOCR1SqUmW9m/eUJNNj4zI2N12i25v8fYpVf+zCakQeaTdBdhnZTFbVIAVZIVVOg== peek-readable@^4.1.0: version "4.1.0" diff --git a/extensions/go/cgmanifest.json b/extensions/go/cgmanifest.json index fc3c741c6f6..bd8f2d6105f 100644 --- a/extensions/go/cgmanifest.json +++ b/extensions/go/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "go-syntax", "repositoryUrl": "https://github.com/worlpaker/go-syntax", - "commitHash": "254bd0f25182c86ffd2043824f8d003e11a34268" + "commitHash": "21f28840e04d4fa04682d19d6fe64de437f40b64" } }, "license": "MIT", "description": "The file syntaxes/go.tmLanguage.json is from https://github.com/worlpaker/go-syntax, which in turn was derived from https://github.com/jeff-hykin/better-go-syntax.", - "version": "0.6.6" + "version": "0.7.5" } ], "version": 1 diff --git a/extensions/go/syntaxes/go.tmLanguage.json b/extensions/go/syntaxes/go.tmLanguage.json index 083d4ffb1a4..b8a6604de88 100644 --- a/extensions/go/syntaxes/go.tmLanguage.json +++ b/extensions/go/syntaxes/go.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/worlpaker/go-syntax/commit/254bd0f25182c86ffd2043824f8d003e11a34268", + "version": "https://github.com/worlpaker/go-syntax/commit/21f28840e04d4fa04682d19d6fe64de437f40b64", "name": "Go", "scopeName": "source.go", "patterns": [ @@ -32,6 +32,9 @@ }, { "include": "#group-variables" + }, + { + "include": "#field_hover" } ] }, @@ -318,7 +321,7 @@ "name": "punctuation.definition.begin.bracket.square.go" } }, - "end": "(?:(\\])((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?!(?:[\\[\\]\\*]+)?\\b(?:func|struct|map)\\b)(?:[\\*\\[\\]]+)?(?:[\\w\\.]+))?)", + "end": "(?:(\\])((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?!(?:[\\[\\]\\*]+)?\\b(?:func|struct|map)\\b)(?:[\\*\\[\\]]+)?(?:[\\w\\.]+)(?:\\[(?:(?:[\\w\\.\\*\\[\\]\\{\\}]+)(?:(?:\\,\\s*(?:[\\w\\.\\*\\[\\]\\{\\}]+))*))?\\])?)?)", "endCaptures": { "1": { "name": "punctuation.definition.end.bracket.square.go" @@ -1859,7 +1862,7 @@ }, { "comment": "property variables and types", - "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))([\\s\\S]+))", + "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))([^\\`]+))", "captures": { "1": { "patterns": [ @@ -2004,6 +2007,29 @@ } ] }, + { + "comment": "one type only with multi line raw string", + "begin": "(?:((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?\\&\\|\\%\\*]+)?)+)?(\\))))", - "captures": { + "begin": "(?:(\\bmake\\b)(?:(\\()((?:(?:(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+(?:\\([^\\)]+\\))?)?(?:[\\[\\]\\*]+)?(?:(?!\\bmap\\b)(?:[\\w\\.]+))?(\\[(?:(?:[\\S]+)(?:(?:\\,\\s*(?:[\\S]+))*))?\\])?(?:\\,)?)?))", + "beginCaptures": { "1": { "name": "entity.name.function.support.builtin.go" }, @@ -2398,18 +2427,19 @@ "name": "entity.name.type.go" } ] - }, - "4": { - "patterns": [ - { - "include": "$self" - } - ] - }, - "5": { + } + }, + "end": "\\)", + "endCaptures": { + "0": { "name": "punctuation.definition.end.bracket.round.go" } - } + }, + "patterns": [ + { + "include": "$self" + } + ] } ] }, @@ -2435,7 +2465,7 @@ }, "switch_types": { "comment": "switch type assertions, only highlights types after case keyword", - "begin": "(?<=\\bswitch\\b)(?:\\s*)(?:(\\w+\\s*\\:\\=)?\\s*([\\w\\.\\*\\(\\)\\[\\]]+))(\\.\\(\\btype\\b\\)\\s*)(\\{)", + "begin": "(?<=\\bswitch\\b)(?:\\s*)(?:(\\w+\\s*\\:\\=)?\\s*([\\w\\.\\*\\(\\)\\[\\]\\+/\\-\\%\\<\\>\\|\\&]+))(\\.\\(\\btype\\b\\)\\s*)(\\{)", "beginCaptures": { "1": { "patterns": [ @@ -2766,7 +2796,7 @@ }, "slice_index_variables": { "comment": "slice index and capacity variables, to not scope them as property variables", - "match": "(?<=\\w\\[)((?:(?:\\b[\\w\\.\\*\\+/\\-\\*\\%\\<\\>\\|\\&]+\\:)|(?:\\:\\b[\\w\\.\\*\\+/\\-\\*\\%\\<\\>\\|\\&]+))(?:\\b[\\w\\.\\*\\+/\\-\\*\\%\\<\\>\\|\\&]+)?(?:\\:\\b[\\w\\.\\*\\+/\\-\\*\\%\\<\\>\\|\\&]+)?)(?=\\])", + "match": "(?<=\\w\\[)((?:(?:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+\\:)|(?:\\:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+))(?:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+)?(?:\\:\\b[\\w\\.\\*\\+/\\-\\%\\<\\>\\|\\&]+)?)(?=\\])", "captures": { "1": { "patterns": [ @@ -2782,8 +2812,8 @@ } }, "property_variables": { - "comment": "Property variables in struct | parameter field in struct initialization", - "match": "(?:(?:((?:\\b[\\w\\.]+)(?:\\:(?!\\=))))(?:(?:\\s*([\\w\\.\\*\\&\\[\\]]+)(\\.\\w+)(?![\\w\\.\\*\\&\\[\\]]*(?:\\{|\\()))((?:\\s*(?:\\<|\\>|\\<\\=|\\>\\=|\\=\\=|\\!\\=|\\|\\||\\&\\&|\\+|/|\\-|\\*|\\%|\\||\\&)\\s*(?:[\\w\\.\\*\\&\\[\\]]+)(?:\\.\\w+)(?![\\w\\.\\*\\&\\[\\]]*(?:\\{|\\()))*))?)", + "comment": "Property variables in struct", + "match": "((?:\\b[\\w\\.]+)(?:\\:(?!\\=)))", "captures": { "1": { "patterns": [ @@ -2795,68 +2825,6 @@ "name": "variable.other.property.go" } ] - }, - "2": { - "patterns": [ - { - "include": "#type-declarations" - }, - { - "match": "\\w+", - "name": "variable.other.go" - }, - { - "include": "$self" - } - ] - }, - "3": { - "patterns": [ - { - "include": "#type-declarations" - }, - { - "match": "\\w+", - "name": "variable.other.property.field.go" - }, - { - "include": "$self" - } - ] - }, - "4": { - "patterns": [ - { - "match": "([\\w\\.\\*\\&\\[\\]]+)(\\.\\w+)", - "captures": { - "1": { - "patterns": [ - { - "include": "#type-declarations" - }, - { - "match": "\\w+", - "name": "variable.other.go" - } - ] - }, - "2": { - "patterns": [ - { - "include": "#type-declarations" - }, - { - "match": "\\w+", - "name": "variable.other.property.field.go" - } - ] - } - } - }, - { - "include": "$self" - } - ] } } }, @@ -2910,6 +2878,41 @@ } } }, + "field_hover": { + "comment": "struct field property and types when hovering with the mouse", + "match": "(?:(?<=^\\bfield\\b)\\s+([\\w\\*\\.]+)\\s+([\\s\\S]+))", + "captures": { + "1": { + "patterns": [ + { + "include": "#type-declarations" + }, + { + "match": "\\w+", + "name": "variable.other.property.go" + } + ] + }, + "2": { + "patterns": [ + { + "match": "\\binvalid\\b\\s+\\btype\\b", + "name": "invalid.field.go" + }, + { + "include": "#type-declarations-without-brackets" + }, + { + "include": "#parameter-variable-types" + }, + { + "match": "\\w+", + "name": "entity.name.type.go" + } + ] + } + } + }, "other_variables": { "comment": "all other variables", "match": "\\w+", diff --git a/extensions/html-language-features/client/src/browser/htmlClientMain.ts b/extensions/html-language-features/client/src/browser/htmlClientMain.ts index 3f10e6d131f..06997d39fb0 100644 --- a/extensions/html-language-features/client/src/browser/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/browser/htmlClientMain.ts @@ -8,13 +8,6 @@ import { LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient'; import { LanguageClient } from 'vscode-languageclient/browser'; -declare const Worker: { - new(stringUrl: string): any; -}; -declare const TextDecoder: { - new(encoding?: string): { decode(buffer: ArrayBuffer): string }; -}; - let client: AsyncDisposable | undefined; // this method is called when vs code is activated @@ -25,7 +18,7 @@ export async function activate(context: ExtensionContext) { worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' }); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, clientOptions, worker); + return new LanguageClient(id, name, worker, clientOptions); }; const timer = { diff --git a/extensions/html-language-features/client/tsconfig.json b/extensions/html-language-features/client/tsconfig.json index 8f5cef74fd3..349af163eea 100644 --- a/extensions/html-language-features/client/tsconfig.json +++ b/extensions/html-language-features/client/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./out" + "outDir": "./out", + "lib": [ + "webworker" + ] }, "include": [ "src/**/*", diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 49489ff20df..ac026b973eb 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -259,7 +259,7 @@ }, "dependencies": { "@vscode/extension-telemetry": "^0.9.0", - "vscode-languageclient": "^10.0.0-next.3", + "vscode-languageclient": "^10.0.0-next.8", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 75bfa00de11..c1ddc242fa4 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -10,9 +10,9 @@ "main": "./out/node/htmlServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.2.13", - "vscode-html-languageservice": "^5.2.0", - "vscode-languageserver": "^10.0.0-next.2", + "vscode-css-languageservice": "^6.3.0", + "vscode-html-languageservice": "^5.3.0", + "vscode-languageserver": "^10.0.0-next.6", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index f327f1f352f..caaf929d895 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -24,38 +24,38 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-css-languageservice@^6.2.13: - version "6.2.13" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.2.13.tgz#c7c2dc7a081a203048d60157c65536767d6d96f8" - integrity sha512-2rKWXfH++Kxd9Z4QuEgd1IF7WmblWWU7DScuyf1YumoGLkY9DW6wF/OTlhOyO2rN63sWHX2dehIpKBbho4ZwvA== +vscode-css-languageservice@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.3.0.tgz#51724d193d19b1a9075b1cef5cfeea6a555d2aa4" + integrity sha512-nU92imtkgzpCL0xikrIb8WvedV553F2BENzgz23wFuok/HLN5BeQmroMy26pUwFxV2eV8oNRmYCUv8iO7kSMhw== dependencies: "@vscode/l10n" "^0.0.18" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "3.17.5" vscode-uri "^3.0.8" -vscode-html-languageservice@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.2.0.tgz#5b36f9131acc073cebaa2074dc8ff53e84c80f31" - integrity sha512-cdNMhyw57/SQzgUUGSIMQ66jikqEN6nBNyhx5YuOyj9310+eY9zw8Q0cXpiKzDX8aHYFewQEXRnigl06j/TVwQ== +vscode-html-languageservice@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.3.0.tgz#298ae5600c6749cbb95838975d07f449c44cb478" + integrity sha512-C4Z3KsP5Ih+fjHpiBc5jxmvCl+4iEwvXegIrzu2F5pktbWvQaBT3YkVPk8N+QlSSMk8oCG6PKtZ/Sq2YHb5e8g== dependencies: "@vscode/l10n" "^0.0.18" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "^3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageserver-protocol@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.3.tgz#09d3e28e9ad12270233d07fa0b69cf1d51d7dfe4" - integrity sha512-H8ATH5SAvc3JzttS+AL6g681PiBOZM/l34WP2JZk4akY3y7NqTP+f9cJ+MhrVBbD3aDS8bdAKewZgbFLW6M8Pg== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" @@ -67,17 +67,17 @@ vscode-languageserver-types@3.17.5, vscode-languageserver-types@^3.17.5: resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== -vscode-languageserver@^10.0.0-next.2: - version "10.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.2.tgz#9a8ac58f72979961497c4fd7f6097561d4134d5f" - integrity sha512-WZdK/XO6EkNU6foYck49NpS35sahWhYFs4hwCGalH/6lhPmdUKABTnWioK/RLZKWqH8E5HdlAHQMfSBIxKBV9Q== +vscode-languageserver@^10.0.0-next.6: + version "10.0.0-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.6.tgz#0db118a93fe010c6b40cd04e91a15d09e7b60b60" + integrity sha512-0Lh1nhQfSxo5Ob+ayYO1QTIsDix2/Lc72Urm1KZrCFxK5zIFYaEh3QFeM9oZih4Rzs0ZkQPXXnoHtpvs5GT+Zw== dependencies: - vscode-languageserver-protocol "3.17.6-next.3" + vscode-languageserver-protocol "3.17.6-next.6" vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/html-language-features/yarn.lock b/extensions/html-language-features/yarn.lock index d1d73407809..aa2ea1c6840 100644 --- a/extensions/html-language-features/yarn.lock +++ b/extensions/html-language-features/yarn.lock @@ -149,32 +149,32 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageclient@^10.0.0-next.3: - version "10.0.0-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.3.tgz#d7336bafafb37569ac1d8e931d20ba2a6385cc64" - integrity sha512-jJhPdZaiELpPRnCUt8kQcF2HJuvzLgeW4HOGc6dp8Je+p08ndueVT4fpSsbly6KiEHr/Ri73tNz0CSfsOye6MA== +vscode-languageclient@^10.0.0-next.8: + version "10.0.0-next.8" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.8.tgz#5afa0ced3b2ac68d31cc1c48edc4f289744542a0" + integrity sha512-D9inIHgqKayO9Tv0MeLb3XIL76yTuWmKdHqcGZKzjtQrMGJgASJDYWTapu+yAjEpDp0gmVOaCYyIlLB86ncDoQ== dependencies: minimatch "^9.0.3" semver "^7.6.0" - vscode-languageserver-protocol "3.17.6-next.4" + vscode-languageserver-protocol "3.17.6-next.6" -vscode-languageserver-protocol@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.4.tgz#3c56f6eb588bb42fccc0ac54a0d5daf2d02f0a1b" - integrity sha512-/2bleKBxZLyRObS4mkpaWlVI9xGiUqMVmh/ztZ2vL4uP2XyIpraT45JBpn9AtXr0alqKJPKLuKr+/qcYULvm/w== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index d923904ef9d..d881eb8ca22 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -15,7 +15,8 @@ ], "activationEvents": [ "onNotebook:jupyter-notebook", - "onNotebookSerializer:interactive" + "onNotebookSerializer:interactive", + "onNotebookSerializer:repl" ], "extensionKind": [ "workspace", @@ -61,6 +62,11 @@ "command": "notebook.cellOutput.copy", "title": "%copyCellOutput.title%", "category": "Notebook" + }, + { + "command": "notebook.cellOutput.openInTextEditor", + "title": "%openCellOutput.title%", + "category": "Notebook" } ], "notebooks": [ @@ -107,12 +113,24 @@ { "command": "notebook.cellOutput.copy", "when": "notebookCellHasOutputs" + }, + { + "command": "notebook.cellOutput.openInTextEditor", + "when": "false" } ], "webview/context": [ { "command": "notebook.cellOutput.copy", "when": "webviewId == 'notebook.output' && webviewSection == 'image'" + }, + { + "command": "notebook.cellOutput.copy", + "when": "webviewId == 'notebook.output' && webviewSection == 'text'" + }, + { + "command": "notebook.cellOutput.openInTextEditor", + "when": "webviewId == 'notebook.output' && webviewSection == 'text'" } ] } diff --git a/extensions/ipynb/package.nls.json b/extensions/ipynb/package.nls.json index af7d8f4ab47..7a3d95181cf 100644 --- a/extensions/ipynb/package.nls.json +++ b/extensions/ipynb/package.nls.json @@ -7,6 +7,7 @@ "openIpynbInNotebookEditor.title": "Open IPYNB File In Notebook Editor", "cleanInvalidImageAttachment.title": "Clean Invalid Image Attachment Reference", "copyCellOutput.title": "Copy Cell Output", + "openCellOutput.title": "Open Cell Output in Text Editor", "markdownAttachmentRenderer.displayName": { "message": "Markdown-It ipynb Cell Attachment renderer", "comment": [ diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts index 889f4c07445..6d73107ef54 100644 --- a/extensions/ipynb/src/ipynbMain.ts +++ b/extensions/ipynb/src/ipynbMain.ts @@ -117,13 +117,6 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(cleaner); } - // Update new file contribution - vscode.extensions.onDidChange(() => { - vscode.commands.executeCommand('setContext', 'jupyterEnabled', vscode.extensions.getExtension('ms-toolsai.jupyter')); - }); - vscode.commands.executeCommand('setContext', 'jupyterEnabled', vscode.extensions.getExtension('ms-toolsai.jupyter')); - - return { get dropCustomMetadata() { return !useCustomPropertyInMetadata(); diff --git a/extensions/javascript/javascript-language-configuration.json b/extensions/javascript/javascript-language-configuration.json index fb2fb0397d7..f7c332337cb 100644 --- a/extensions/javascript/javascript-language-configuration.json +++ b/extensions/javascript/javascript-language-configuration.json @@ -111,10 +111,10 @@ }, "indentationRules": { "decreaseIndentPattern": { - "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]\\)].*$" + "pattern": "^\\s*[\\}\\]\\)].*$" }, "increaseIndentPattern": { - "pattern": "^((?!//).)*(\\{([^}\"'`/]*|(\\t|[ ])*//.*)|\\([^)\"'`/]*|\\[[^\\]\"'`/]*)$" + "pattern": "^.*(\\{[^}]*|\\([^)]*|\\[[^\\]]*)$" }, // e.g. * ...| or */| or *-----*/| "unIndentedLinePattern": { diff --git a/extensions/javascript/snippets/javascript.code-snippets b/extensions/javascript/snippets/javascript.code-snippets index 9448fd140d9..5bf6aa5edee 100644 --- a/extensions/javascript/snippets/javascript.code-snippets +++ b/extensions/javascript/snippets/javascript.code-snippets @@ -1,16 +1,79 @@ { - "define module": { - "prefix": "define", + "Constructor": { + "prefix": "ctor", "body": [ - "define([", - "\t'require',", - "\t'${1:dependency}'", - "], function(require, ${2:factory}) {", - "\t'use strict';", + "/**", + " *", + " */", + "constructor() {", + "\tsuper();", "\t$0", - "});" + "}" ], - "description": "define module" + "description": "Constructor" + }, + "Class Definition": { + "prefix": "class", + "isFileTemplate": true, + "body": [ + "class ${1:name} {", + "\tconstructor(${2:parameters}) {", + "\t\t$0", + "\t}", + "}" + ], + "description": "Class Definition" + }, + "Method Definition": { + "prefix": "method", + "body": [ + "/**", + " * ", + " */", + "${1:name}() {", + "\t$0", + "}" + ], + "description": "Method Definition" + }, + "Import Statement": { + "prefix": "import", + "body": [ + "import { $0 } from \"${1:module}\";" + ], + "description": "Import external module" + }, + "Log to the console": { + "prefix": "log", + "body": [ + "console.log($1);", + "$0" + ], + "description": "Log to the console" + }, + "Log warning to console": { + "prefix": "warn", + "body": [ + "console.warn($1);", + "$0" + ], + "description": "Log warning to the console" + }, + "Log error to console": { + "prefix": "error", + "body": [ + "console.error($1);", + "$0" + ], + "description": "Log error to the console" + }, + "Throw Exception": { + "prefix": "throw", + "body": [ + "throw new Error(\"$1\");", + "$0" + ], + "description": "Throw Exception" }, "For Loop": { "prefix": "for", @@ -22,20 +85,20 @@ ], "description": "For Loop" }, - "For-Each Loop": { - "prefix": "foreach", + "For-Each Loop using =>": { + "prefix": "foreach =>", "body": [ "${1:array}.forEach(${2:element} => {", "\t$TM_SELECTED_TEXT$0", "});" ], - "description": "For-Each Loop" + "description": "For-Each Loop using =>" }, "For-In Loop": { "prefix": "forin", "body": [ "for (const ${1:key} in ${2:object}) {", - "\tif (Object.hasOwnProperty.call(${2:object}, ${1:key})) {", + "\tif (Object.prototype.hasOwnProperty.call(${2:object}, ${1:key})) {", "\t\tconst ${3:element} = ${2:object}[${1:key}];", "\t\t$TM_SELECTED_TEXT$0", "\t}", @@ -46,12 +109,21 @@ "For-Of Loop": { "prefix": "forof", "body": [ - "for (const ${1:iterator} of ${2:object}) {", + "for (const ${1:element} of ${2:object}) {", "\t$TM_SELECTED_TEXT$0", "}" ], "description": "For-Of Loop" }, + "For-Await-Of Loop": { + "prefix": "forawaitof", + "body": [ + "for await (const ${1:element} of ${2:object}) {", + "\t$TM_SELECTED_TEXT$0", + "}" + ], + "description": "For-Await-Of Loop" + }, "Function Statement": { "prefix": "function", "body": [ @@ -149,13 +221,6 @@ ], "description": "Set Interval Function" }, - "Import Statement": { - "prefix": "import", - "body": [ - "import { $0 } from \"${1:module}\";" - ], - "description": "Import external module" - }, "Region Start": { "prefix": "#region", "body": [ @@ -170,27 +235,6 @@ ], "description": "Folding Region End" }, - "Log to the console": { - "prefix": "log", - "body": [ - "console.log($1);" - ], - "description": "Log to the console" - }, - "Log warning to console": { - "prefix": "warn", - "body": [ - "console.warn($1);" - ], - "description": "Log warning to the console" - }, - "Log error to console": { - "prefix": "error", - "body": [ - "console.error($1);" - ], - "description": "Log error to the console" - }, "new Promise": { "prefix": "newpromise", "body": [ @@ -199,5 +243,23 @@ "})" ], "description": "Create a new Promise" + }, + "Async Function Statement": { + "prefix": "async function", + "body": [ + "async function ${1:name}(${2:params}) {", + "\t$TM_SELECTED_TEXT$0", + "}" + ], + "description": "Async Function Statement" + }, + "Async Function Expression": { + "prefix": "async arrow function", + "body": [ + "async (${1:params}) => {", + "\t$TM_SELECTED_TEXT$0", + "}" + ], + "description": "Async Function Expression" } } diff --git a/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/extensions/json-language-features/client/src/browser/jsonClientMain.ts index f78f494d727..91ed937fe6f 100644 --- a/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -8,12 +8,6 @@ import { LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable, languageServerDescription } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; -declare const Worker: { - new(stringUrl: string): any; -}; - -declare function fetch(uri: string, options: any): any; - let client: AsyncDisposable | undefined; // this method is called when vs code is activated @@ -24,7 +18,7 @@ export async function activate(context: ExtensionContext) { worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' }); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, clientOptions, worker); + return new LanguageClient(id, name, worker, clientOptions); }; const schemaRequests: SchemaRequestService = { diff --git a/extensions/json-language-features/client/tsconfig.json b/extensions/json-language-features/client/tsconfig.json index aa51e4d0157..89e6a6c12b7 100644 --- a/extensions/json-language-features/client/tsconfig.json +++ b/extensions/json-language-features/client/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./out" + "outDir": "./out", + "lib": [ + "webworker" + ] }, "include": [ "src/**/*", diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index f86470429a4..fa8004e2b02 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -163,7 +163,7 @@ "dependencies": { "@vscode/extension-telemetry": "^0.9.0", "request-light": "^0.7.0", - "vscode-languageclient": "^10.0.0-next.5" + "vscode-languageclient": "^10.0.0-next.8" }, "devDependencies": { "@types/node": "20.x" diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 6134fb4224d..8472ca618a4 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -15,8 +15,8 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.2.1", "request-light": "^0.7.0", - "vscode-json-languageservice": "^5.3.11", - "vscode-languageserver": "^10.0.0-next.3", + "vscode-json-languageservice": "^5.4.0", + "vscode-languageserver": "^10.0.0-next.6", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 669e823497d..608619637e4 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -24,6 +24,11 @@ jsonc-parser@^3.2.1: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== +jsonc-parser@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.0.tgz#030d182672c8ffc2805db95467c83ffc0b033d9d" + integrity sha512-RK1Xb5alM78sdXpB2hqqK7jxAE5jTRH05GvUiLWqh7Vbp6OPHuJYlsAMRUDYNYJTAQgkmhHgkdwOEknxwP4ojQ== + request-light@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.7.0.tgz#885628bb2f8040c26401ebf258ec51c4ae98ac2a" @@ -34,51 +39,51 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-json-languageservice@^5.3.11: - version "5.3.11" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-5.3.11.tgz#71dbc56e9b1d07a57aa6a3d5569c8b7f2c05ca05" - integrity sha512-WYS72Ymria3dn8ZbjtBbt5K71m05wY1Q6hpXV5JxUT0q75Ts0ljLmnZJAVpx8DjPgYbFD+Z8KHpWh2laKLUCtQ== +vscode-json-languageservice@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-5.4.0.tgz#caf1aabc81b1df9faf6a97e4c34e13a2d10a8cdf" + integrity sha512-NCkkCr63OHVkE4lcb0xlUAaix6vE5gHQW4NrswbLEh3ArXj81lrGuFTsGEYEUXlNHdnc53vWPcjeSy/nMTrfXg== dependencies: "@vscode/l10n" "^0.0.18" - jsonc-parser "^3.2.1" + jsonc-parser "^3.3.0" vscode-languageserver-textdocument "^1.0.11" vscode-languageserver-types "^3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageserver-protocol@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.4.tgz#3c56f6eb588bb42fccc0ac54a0d5daf2d02f0a1b" - integrity sha512-/2bleKBxZLyRObS4mkpaWlVI9xGiUqMVmh/ztZ2vL4uP2XyIpraT45JBpn9AtXr0alqKJPKLuKr+/qcYULvm/w== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== vscode-languageserver-types@^3.17.5: version "3.17.5" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== -vscode-languageserver@^10.0.0-next.3: - version "10.0.0-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.3.tgz#a63c5ea9fab1be93d7732ab0fdc18c9b37956e07" - integrity sha512-4x1qHImf6ePji4+8PX43lnBCBfBNdi2jneGX2k5FswJhx/cxaYYmusShmmtO/clyL1iurxJacrQoXfw9+ikhvg== +vscode-languageserver@^10.0.0-next.6: + version "10.0.0-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.6.tgz#0db118a93fe010c6b40cd04e91a15d09e7b60b60" + integrity sha512-0Lh1nhQfSxo5Ob+ayYO1QTIsDix2/Lc72Urm1KZrCFxK5zIFYaEh3QFeM9oZih4Rzs0ZkQPXXnoHtpvs5GT+Zw== dependencies: - vscode-languageserver-protocol "3.17.6-next.4" + vscode-languageserver-protocol "3.17.6-next.6" vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index b7ca937103a..c825de07683 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -154,32 +154,32 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-jsonrpc@9.0.0-next.2: - version "9.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.2.tgz#29e9741c742c80329bba1c60ce38fd014651ba80" - integrity sha512-meIaXAgChCHzWy45QGU8YpCNyqnZQ/sYeCj32OLDDbUYsCF7AvgpdXx3nnZn9yzr8ed0Od9bW+NGphEmXsqvIQ== +vscode-jsonrpc@9.0.0-next.4: + version "9.0.0-next.4" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" + integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== -vscode-languageclient@^10.0.0-next.5: - version "10.0.0-next.5" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.5.tgz#7431d88255a5fd99e9423659ac484b1f968200f3" - integrity sha512-JIf1WE7fvV0RElFM062bAummI433vcxuFwqoYAp+1zTVhta/jznxkTz1zs3Hbj2tiDfclf0TZ0qCxflAP1mY2Q== +vscode-languageclient@^10.0.0-next.8: + version "10.0.0-next.8" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.8.tgz#5afa0ced3b2ac68d31cc1c48edc4f289744542a0" + integrity sha512-D9inIHgqKayO9Tv0MeLb3XIL76yTuWmKdHqcGZKzjtQrMGJgASJDYWTapu+yAjEpDp0gmVOaCYyIlLB86ncDoQ== dependencies: minimatch "^9.0.3" semver "^7.6.0" - vscode-languageserver-protocol "3.17.6-next.4" + vscode-languageserver-protocol "3.17.6-next.6" -vscode-languageserver-protocol@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.4.tgz#3c56f6eb588bb42fccc0ac54a0d5daf2d02f0a1b" - integrity sha512-/2bleKBxZLyRObS4mkpaWlVI9xGiUqMVmh/ztZ2vL4uP2XyIpraT45JBpn9AtXr0alqKJPKLuKr+/qcYULvm/w== +vscode-languageserver-protocol@3.17.6-next.6: + version "3.17.6-next.6" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" + integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== dependencies: - vscode-jsonrpc "9.0.0-next.2" - vscode-languageserver-types "3.17.6-next.3" + vscode-jsonrpc "9.0.0-next.4" + vscode-languageserver-types "3.17.6-next.4" -vscode-languageserver-types@3.17.6-next.3: - version "3.17.6-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.3.tgz#f71d6c57f18d921346cfe0c227aabd72eb8cd2f0" - integrity sha512-l5kNFXFRQGuzriXpuBqFpRmkf6f6A4VoU3h95OsVkqIOoi1k7KbwSo600cIdsKSJWrPg/+vX+QMPcMw1oI7ItA== +vscode-languageserver-types@3.17.6-next.4: + version "3.17.6-next.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" + integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== yallist@^4.0.0: version "4.0.0" diff --git a/extensions/latex/cgmanifest.json b/extensions/latex/cgmanifest.json index 609d875ac2f..b537c48ee8c 100644 --- a/extensions/latex/cgmanifest.json +++ b/extensions/latex/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jlelong/vscode-latex-basics", "repositoryUrl": "https://github.com/jlelong/vscode-latex-basics", - "commitHash": "56e2dc967e6bafafc1acfeeb80af42b8328b021a" + "commitHash": "5d7c2a4e451a932b776f6d9342087be6a1e8c0a1" } }, "license": "MIT", - "version": "1.7.0", + "version": "1.9.0", "description": "The files in syntaxes/ were originally part of https://github.com/James-Yu/LaTeX-Workshop. They have been extracted in the hope that they can useful outside of the LaTeX-Workshop extension.", "licenseDetail": [ "Copyright (c) vscode-latex-basics authors", diff --git a/extensions/latex/syntaxes/LaTeX.tmLanguage.json b/extensions/latex/syntaxes/LaTeX.tmLanguage.json index aad6c5c4bd9..76486732195 100644 --- a/extensions/latex/syntaxes/LaTeX.tmLanguage.json +++ b/extensions/latex/syntaxes/LaTeX.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jlelong/vscode-latex-basics/commit/3d141a124a16558958e95c54267f7ca37986de6f", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/9cd6bc151f4b9df5d9aeb1e39e30071018d3cb2a", "name": "LaTeX", "scopeName": "text.tex.latex", "patterns": [ @@ -2646,7 +2646,7 @@ ] }, { - "begin": "((\\\\)(?:\\w*[rR]ef\\*?))(\\{)", + "begin": "((\\\\)(?:\\w*[rR]ef\\*?))(?:\\[[^\\]]*\\])?(\\{)", "beginCaptures": { "1": { "name": "keyword.control.ref.latex" @@ -3048,7 +3048,7 @@ "name": "punctuation.definition.variable.latex" } }, - "match": "(\\\\)[cgl](?:[_\\p{Alphabetic}@]+)+_(?:bitset|clist|dim|fp|int|muskip|str|tl|bool|box|coffin|flag|fparray|intarray|ior|iow|prop|regex|seq)", + "match": "(\\\\)[cgl](?:[_\\p{Alphabetic}@]+)+_[a-z]+", "name": "variable.other.latex3.latex" }, { @@ -3148,7 +3148,7 @@ "match": "\\s*((\\\\)(?:begin|end))(\\{)([a-zA-Z]*\\*?)(\\})(?:(\\[)([^\\]]*)(\\])){,2}(?:(\\{)([^{}]*)(\\}))?" }, "definition-label": { - "begin": "((\\\\)label)((?:\\[[^\\[]*?\\])*)(\\{)", + "begin": "((\\\\)z?label)((?:\\[[^\\[]*?\\])*)(\\{)", "beginCaptures": { "1": { "name": "keyword.control.label.latex" diff --git a/extensions/latex/syntaxes/TeX.tmLanguage.json b/extensions/latex/syntaxes/TeX.tmLanguage.json index 205d8bdfce0..0cb03e61466 100644 --- a/extensions/latex/syntaxes/TeX.tmLanguage.json +++ b/extensions/latex/syntaxes/TeX.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jlelong/vscode-latex-basics/commit/8624d0bdae950a70cdf4a1c3d19c7398ef851721", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/5d7c2a4e451a932b776f6d9342087be6a1e8c0a1", "name": "TeX", "scopeName": "text.tex", "patterns": [ @@ -108,7 +108,25 @@ "name": "punctuation.definition.function.tex" } }, - "match": "(\\\\)(?:[,;]|(?:[\\p{Alphabetic}@]+(?:(?:_[\\p{Alphabetic}@]+)*:[NncVvoxefTFpwD]*)?))", + "match": "(\\\\)_*[\\p{Alphabetic}@]+(?:_[\\p{Alphabetic}@]+)*:[NncVvoxefTFpwD]*", + "name": "support.class.general.latex3.tex" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.function.tex" + } + }, + "match": "(\\.)[\\p{Alphabetic}@]+(?:_[\\p{Alphabetic}@]+)*:[NncVvoxefTFpwD]*", + "name": "support.class.general.latex3.tex" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.function.tex" + } + }, + "match": "(\\\\)(?:[,;]|(?:[\\p{Alphabetic}@]+))", "name": "support.function.general.tex" }, { diff --git a/extensions/less/cgmanifest.json b/extensions/less/cgmanifest.json index caf908bbcc0..a8d1702aa82 100644 --- a/extensions/less/cgmanifest.json +++ b/extensions/less/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "language-less", "repositoryUrl": "https://github.com/radium-v/Better-Less", - "commitHash": "24047277622c245dbe9309f0004d0ccb8f02636f" + "commitHash": "b06a4555c711a6ef0d76cf2b4fc8b929a6ce551a" } }, "license": "MIT", diff --git a/extensions/less/syntaxes/less.tmLanguage.json b/extensions/less/syntaxes/less.tmLanguage.json index 2acac688385..cea782810fb 100644 --- a/extensions/less/syntaxes/less.tmLanguage.json +++ b/extensions/less/syntaxes/less.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/radium-v/Better-Less/commit/24047277622c245dbe9309f0004d0ccb8f02636f", + "version": "https://github.com/radium-v/Better-Less/commit/b06a4555c711a6ef0d76cf2b4fc8b929a6ce551a", "name": "Less", "scopeName": "source.css.less", "patterns": [ @@ -615,6 +615,9 @@ { "include": "#filter-function" }, + { + "include": "#fit-content-function" + }, { "include": "#format-function" }, @@ -738,6 +741,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#comma-delimiter" }, @@ -781,6 +787,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#comma-delimiter" }, @@ -813,6 +822,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "match": "\\b(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)\\b", "name": "support.constant.color.w3c-standard-color-name.less" @@ -902,6 +914,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "match": "(?:--(?:[[-\\w][^\\x{00}-\\x{7F}]]|(?:\\\\\\h{1,6}[\\s\\t\\n\\f]?|\\\\[^\\n\\f\\h]))+|-?(?:[[_a-zA-Z][^\\x{00}-\\x{7F}]]|(?:\\\\\\h{1,6}[\\s\\t\\n\\f]?|\\\\[^\\n\\f\\h]))(?:[[-\\w][^\\x{00}-\\x{7F}]]|(?:\\\\\\h{1,6}[\\s\\t\\n\\f]?|\\\\[^\\n\\f\\h]))*)", "name": "entity.other.counter-name.less" @@ -961,6 +976,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#literal-string" }, @@ -1053,6 +1071,9 @@ }, "end": "(?=\\))", "patterns": [ + { + "include": "#var-function" + }, { "include": "#comma-delimiter" }, @@ -1275,6 +1296,49 @@ } ] }, + "fit-content-function": { + "begin": "\\b(fit-content)(?=\\()", + "beginCaptures": { + "1": { + "name": "support.function.grid.less" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.end.less" + } + }, + "name": "meta.function-call.less", + "patterns": [ + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.group.begin.less" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "include": "#less-variables" + }, + { + "include": "#var-function" + }, + { + "include": "#calc-function" + }, + { + "include": "#length-type" + }, + { + "include": "#percentage-type" + } + ] + } + ] + }, "format-function": { "patterns": [ { @@ -1348,6 +1412,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#angle-type" }, @@ -1402,6 +1469,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#color-values" }, @@ -1628,6 +1698,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#comma-delimiter" }, @@ -1704,6 +1777,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#color-values" } @@ -3360,6 +3436,9 @@ { "include": "#less-variables" }, + { + "include": "#var-function" + }, { "include": "#length-type" }, @@ -3421,7 +3500,22 @@ "property-value-constants": { "patterns": [ { - "match": "(?x)\\b(\n absolute|active|add\n |all(-(petite|small)-caps|-scroll)?\n |alpha(betic)?\n |alternate(-reverse)?\n |always|annotation|antialiased|at\n |auto(hiding-scrollbar)?\n |avoid(-column|-page|-region)?\n |background(-color|-image|-position|-size)?\n |backwards|balance|baseline|below|bevel|bicubic|bidi-override|blink\n |block(-line-height)?\n |blur\n |bold(er)?\n |border(-bottom|-left|-right|-top)?-(color|radius|width|style)\n |border-(bottom|top)-(left|right)-radius\n |border-image(-outset|-repeat|-slice|-source|-width)?\n |border(-bottom|-left|-right|-top|-collapse|-spacing|-box)?\n |both|bottom\n |box(-shadow)?\n |break-(all|word)\n |brightness\n |butt(on)?\n |capitalize\n |cent(er|ral)\n |char(acter-variant)?\n |cjk-ideographic|clip|clone|close-quote\n |closest-(corner|side)\n |col-resize|collapse\n |color(-stop|-burn|-dodge)?\n |column((-count|-gap|-reverse|-rule(-color|-width)?|-width)|s)?\n |common-ligatures|condensed|consider-shifts|contain\n |content(-box|s)?\n |contextual|contrast|cover\n |crisp(-e|E)dges\n |crop\n |cross(hair)?\n |da(rken|shed)\n |default|dense|diagonal-fractions|difference|disabled\n |discretionary-ligatures|disregard-shifts\n |distribute(-all-lines|-letter|-space)?\n |dotted|double|drop-shadow\n |(nwse|nesw|ns|ew|sw|se|nw|ne|w|s|e|n)-resize\n |ease(-in-out|-in|-out)?\n |element|ellipsis|embed|end|EndColorStr|evenodd\n |exclu(de(-ruby)?|sion)\n |expanded\n |(extra|semi|ultra)-(condensed|expanded)\n |farthest-(corner|side)?\n |fill(-box|-opacity)?\n |filter|fixed|flat\n |flex((-basis|-end|-grow|-shrink|-start)|box)?\n |flip|flood-color\n |font(-size(-adjust)?|-stretch|-weight)?\n |forwards\n |from(-image)?\n |full-width|geometricPrecision|glyphs|gradient|grayscale\n |grid(-height)?\n |groove|hand|hanging|hard-light|height|help|hidden|hide\n |historical-(forms|ligatures)\n |horizontal(-tb)?\n |hue\n |ideograph(-alpha|-numeric|-parenthesis|-space|ic)\n |inactive|include-ruby|infinite|inherit|initial\n |inline(-block|-box|-flex(box)?|-line-height|-table)?\n |inset|inside\n |inter(-ideograph|-word|sect)\n |invert|isolat(e|ion)|italic\n |jis(04|78|83|90)\n |justify(-all)?\n |keep-all\n |large[r]?\n |last|layout|left|letter-spacing\n |light(e[nr]|ing-color)\n |line(-edge|-height|-through)?\n |linear(-gradient|RGB)?\n |lining-nums|list-item|local|loose|lowercase|lr-tb|ltr\n |lumin(osity|ance)|manual\n |manipulation\n |margin(-bottom|-box|-left|-right|-top)?\n |marker(-offset|s)?\n |mathematical\n |max-(content|height|lines|size|width)\n |medium|middle\n |min-(content|height|width)\n |miter|mixed|move|multiply|newspaper\n |no-(change|clip|(close|open)-quote|(common|discretionary|historical)-ligatures|contextual|drop|repeat)\n |none|nonzero|normal|not-allowed|nowrap|oblique\n |offset(-after|-before|-end|-start)?\n |oldstyle-nums|opacity|open-quote\n |optimize(Legibility|Precision|Quality|Speed)\n |order|ordinal|ornaments\n |outline(-color|-offset|-width)?\n |outset|outside|over(line|-edge|lay)\n |padding(-bottom|-box|-left|-right|-top|-box)?\n |page|painted|paused\n |pan-(x|left|right|y|up|down)\n |perspective-origin\n |petite-caps|pixelated|pointer\n |pinch-zoom\n |pre(-line|-wrap)?\n |preserve-3d\n |progid:DXImageTransform.Microsoft.(Alpha|Blur|dropshadow|gradient|Shadow)\n |progress\n |proportional-(nums|width)\n |radial-gradient|recto|region|relative\n |repeat(-[xy])?\n |repeating-(linear|radial)-gradient\n |replaced|reset-size|reverse|ridge|right\n |round\n |row(-resize|-reverse)?\n |rtl|ruby|running|saturat(e|ion)|screen\n |scroll(-position|bar)?\n |separate|sepia\n |scale-down\n |shape-(image-threshold|margin|outside)\n |show\n |sideways(-lr|-rl)?\n |simplified\n |size\n |slashed-zero|slice\n |small(-caps|er)?\n |smooth|snap|solid|soft-light\n |space(-around|-between)?\n |span|sRGB\n |stack(ed-fractions)?\n |start(ColorStr)?\n |static\n |step-(end|start)\n |sticky\n |stop-(color|opacity)\n |stretch|strict\n |stroke(-box|-dash(array|offset)|-miterlimit|-opacity|-width)?\n |style(set)?\n |stylistic\n |sub(grid|pixel-antialiased|tract)?\n |super|swash\n |table(-caption|-cell|(-column|-footer|-header|-row)-group|-column|-row)?\n |tabular-nums|tb-rl\n |text((-bottom|-(decoration|emphasis)-color|-indent|-(over|under)-edge|-shadow|-size(-adjust)?|-top)|field)?\n |thi(ck|n)\n |titling-ca(ps|se)\n |to[p]?\n |touch|traditional\n |transform(-origin)?\n |under(-edge|line)?\n |unicase|unset|uppercase|upright\n |use-(glyph-orientation|script)\n |verso\n |vertical(-align|-ideographic|-lr|-rl|-text)?\n |view-box\n |viewport-fill(-opacity)?\n |visibility\n |visible(Fill|Painted|Stroke)?\n |wait|wavy|weight|whitespace|(device-)?width|word-spacing\n |wrap(-reverse)?\n |x{1,2}-(large|small)\n |z-index|zero\n |zoom(-in|-out)?\n |((?xi:arabic-indic|armenian|bengali|cambodian|circle|cjk-decimal|cjk-earthly-branch|cjk-heavenly-stem|decimal-leading-zero|decimal|devanagari|disclosure-closed|disclosure-open|disc|ethiopic-numeric|georgian|gujarati|gurmukhi|hebrew|hiragana-iroha|hiragana|japanese-formal|japanese-informal|kannada|katakana-iroha|katakana|khmer|korean-hangul-formal|korean-hanja-formal|korean-hanja-informal|lao|lower-alpha|lower-armenian|lower-greek|lower-latin|lower-roman|malayalam|mongolian|myanmar|oriya|persian|simp-chinese-formal|simp-chinese-informal|square|tamil|telugu|thai|tibetan|trad-chinese-formal|trad-chinese-informal|upper-alpha|upper-armenian|upper-latin|upper-roman)))\\b", + "comment": "align-content, align-items, align-self, justify-content, justify-items, justify-self", + "match": "(?x)\\b(?:\n flex-start|flex-end|start|end|space-between|space-around|space-evenly\n |stretch|baseline|safe|unsafe|legacy|anchor-center|first|last|self-start|self-end\n)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "alignment-baseline", + "match": "(?x)\\b(?:\n text-before-edge|before-edge|middle|central|text-after-edge\n |after-edge|ideographic|alphabetic|hanging|mathematical|top|center|bottom\n)\\b", + "name": "support.constant.property-value.less" + }, + { + "comment": "all/global values", + "match": "\\b(?:initial|inherit|unset|revert-layer|revert)\\b", + "name": "support.constant.property-value.less" + }, + { + "match": "(?x)\\b(\n absolute|active|add\n|all(-(petite|small)-caps|-scroll)?\n|alpha(betic)?\n|alternate(-reverse)?\n|always|annotation|antialiased|at\n|auto(hiding-scrollbar)?\n|avoid(-column|-page|-region)?\n|background(-color|-image|-position|-size)?\n|backwards|balance|baseline|below|bevel|bicubic|bidi-override|blink\n|block(-(line-height|start|end))?\n|blur\n|bold(er)?\n|border(-bottom|-left|-right|-top)?-(color|radius|width|style)\n|border-(bottom|top)-(left|right)-radius\n|border-image(-outset|-repeat|-slice|-source|-width)?\n|border(-bottom|-left|-right|-top|-collapse|-spacing|-box)?\n|both|bottom\n|box(-shadow)?\n|break-(all|word|spaces)\n|brightness\n|butt(on)?\n|capitalize\n|cent(er|ral)\n|char(acter-variant)?\n|cjk-ideographic|clip|clone|close-quote\n|closest-(corner|side)\n|col-resize|collapse\n|color(-stop|-burn|-dodge)?\n|column((-count|-gap|-reverse|-rule(-color|-width)?|-width)|s)?\n|common-ligatures|condensed|consider-shifts|contain\n|content(-box|s)?\n|contextual|contrast|cover\n|crisp(-e|E)dges\n|crop\n|cross(hair)?\n|da(rken|shed)\n|default|dense|diagonal-fractions|difference|disabled\n|discard|discretionary-ligatures|disregard-shifts\n|distribute(-all-lines|-letter|-space)?\n|dotted|double|drop-shadow\n|(nwse|nesw|ns|ew|sw|se|nw|ne|w|s|e|n)-resize\n|ease(-in-out|-in|-out)?\n|element|ellipsis|embed|end|EndColorStr|evenodd\n|exclu(de(-ruby)?|sion)\n|expanded\n|(extra|semi|ultra)-(condensed|expanded)\n|farthest-(corner|side)?\n|fill(-box|-opacity)?\n|filter\n|fit-content\n|fixed\n|flat\n|flex((-basis|-end|-grow|-shrink|-start)|box)?\n|flip|flood-color\n|font(-size(-adjust)?|-stretch|-weight)?\n|forwards\n|from(-image)?\n|full-width|gap|geometricPrecision|glyphs|gradient|grayscale\n|grid((-column|-row)?-gap|-height)?\n|groove|hand|hanging|hard-light|height|help|hidden|hide\n|historical-(forms|ligatures)\n|horizontal(-tb)?\n|hue\n|ideograph(-alpha|-numeric|-parenthesis|-space|ic)\n|inactive|include-ruby|infinite|inherit|initial\n|inline(-(block|box|flex(box)?|line-height|table|start|end))?\n|inset|inside\n|inter(-ideograph|-word|sect)\n|invert|isolat(e|ion)|italic\n|jis(04|78|83|90)\n|justify(-all)?\n|keep-all\n|large[r]?\n|last|layout|left|letter-spacing\n|light(e[nr]|ing-color)\n|line(-edge|-height|-through)?\n|linear(-gradient|RGB)?\n|lining-nums|list-item|local|loose|lowercase|lr-tb|ltr\n|lumin(osity|ance)|manual\n|manipulation\n|margin(-bottom|-box|-left|-right|-top)?\n|marker(-offset|s)?\n|match-parent\n|mathematical\n|max-(content|height|lines|size|width)\n|medium|middle\n|min-(content|height|width)\n|miter|mixed|move|multiply|newspaper\n|no-(change|clip|(close|open)-quote|(common|discretionary|historical)-ligatures|contextual|drop|repeat)\n|none|nonzero|normal|not-allowed|nowrap|oblique\n|offset(-after|-before|-end|-start)?\n|oldstyle-nums|opacity|open-quote\n|optimize(Legibility|Precision|Quality|Speed)\n|order|ordinal|ornaments\n|outline(-color|-offset|-width)?\n|outset|outside|over(line|-edge|lay)\n|padding(-bottom|-box|-left|-right|-top|-box)?\n|page|paint(ed)?|paused\n|pan-(x|left|right|y|up|down)\n|perspective-origin\n|petite-caps|pixelated|pointer\n|pinch-zoom\n|pretty\n|pre(-line|-wrap)?\n|preserve(-3d|-breaks|-spaces)?\n|progid:DXImageTransform.Microsoft.(Alpha|Blur|dropshadow|gradient|Shadow)\n|progress\n|proportional-(nums|width)\n|radial-gradient|recto|region|relative\n|repeat(-[xy])?\n|repeating-(linear|radial)-gradient\n|replaced|reset-size|reverse|revert(-layer)?|ridge|right\n|round\n|row(-gap|-resize|-reverse)?\n|rtl|ruby|running|saturat(e|ion)|screen\n|scroll(-position|bar)?\n|separate|sepia\n|scale-down\n|shape-(image-threshold|margin|outside)\n|show\n|sideways(-lr|-rl)?\n|simplified\n|size\n|slashed-zero|slice\n|small(-caps|er)?\n|smooth|snap|solid|soft-light\n|space(-around|-between)?\n|span|sRGB\n|stable\n|stack(ed-fractions)?\n|start(ColorStr)?\n|static\n|step-(end|start)\n|sticky\n|stop-(color|opacity)\n|stretch|strict\n|stroke(-box|-dash(array|offset)|-miterlimit|-opacity|-width)?\n|style(set)?\n|stylistic\n|sub(grid|pixel-antialiased|tract)?\n|super|swash\n|table(-caption|-cell|(-column|-footer|-header|-row)-group|-column|-row)?\n|tabular-nums|tb-rl\n|text((-bottom|-(decoration|emphasis)-color|-indent|-(over|under)-edge|-shadow|-size(-adjust)?|-top)|field)?\n|thi(ck|n)\n|titling-ca(ps|se)\n|to[p]?\n|touch|traditional\n|transform(-origin)?\n|under(-edge|line)?\n|unicase|unset|uppercase|upright\n|use-(glyph-orientation|script)\n|verso\n|vertical(-align|-ideographic|-lr|-rl|-text)?\n|view-box\n|viewport-fill(-opacity)?\n|visibility\n|visible(Fill|Painted|Stroke)?\n|wait|wavy|weight|whitespace|(device-)?width|word-spacing\n|wrap(-reverse)?\n|x{1,2}-(large|small)\n|z-index|zero\n|zoom(-in|-out)?\n|((?xi:arabic-indic|armenian|bengali|cambodian|circle|cjk-decimal|cjk-earthly-branch|cjk-heavenly-stem|decimal-leading-zero|decimal|devanagari|disclosure-closed|disclosure-open|disc|ethiopic-numeric|georgian|gujarati|gurmukhi|hebrew|hiragana-iroha|hiragana|japanese-formal|japanese-informal|kannada|katakana-iroha|katakana|khmer|korean-hangul-formal|korean-hanja-formal|korean-hanja-informal|lao|lower-alpha|lower-armenian|lower-greek|lower-latin|lower-roman|malayalam|mongolian|myanmar|oriya|persian|simp-chinese-formal|simp-chinese-informal|square|tamil|telugu|thai|tibetan|trad-chinese-formal|trad-chinese-informal|upper-alpha|upper-armenian|upper-latin|upper-roman)))\\b", "name": "support.constant.property-value.less" }, { @@ -3444,9 +3538,6 @@ { "include": "#color-functions" }, - { - "include": "#less-math" - }, { "include": "#less-functions" }, @@ -3465,9 +3556,15 @@ { "include": "#property-value-constants" }, + { + "include": "#less-math" + }, { "include": "#literal-string" }, + { + "include": "#comma-delimiter" + }, { "captures": { "1": { @@ -3909,6 +4006,11 @@ "include": "#property-values" }, { + "captures": { + "1": { + "name": "punctuation.definition.arbitrary-repetition.less" + } + }, "match": "\\s*(?:(,))" } ] @@ -3918,9 +4020,6 @@ { "begin": "\\b(transition(-(property|duration|delay|timing-function))?)\\b", "beginCaptures": { - "0": { - "name": "meta.property-name.less" - }, "1": { "name": "support.type.property-name.less" } @@ -3933,35 +4032,41 @@ }, "patterns": [ { - "captures": { + "begin": "(((\\+_?)?):)(?=[\\s\\t]*)", + "beginCaptures": { "1": { "name": "punctuation.separator.key-value.less" - }, - "4": { - "name": "meta.property-value.less" } }, - "match": "(((\\+_?)?):)([\\s\\t]*)" - }, - { - "include": "#time-type" - }, - { - "include": "#property-values" - }, - { - "include": "#cubic-bezier-function" - }, - { - "include": "#steps-function" - }, - { "captures": { "1": { "name": "punctuation.definition.arbitrary-repetition.less" } }, - "match": "\\s*(?:(,))" + "contentName": "meta.property-value.less", + "end": "(?=\\s*(;)|(?=[})]))", + "patterns": [ + { + "include": "#time-type" + }, + { + "include": "#property-values" + }, + { + "include": "#cubic-bezier-function" + }, + { + "include": "#steps-function" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.arbitrary-repetition.less" + } + }, + "match": "\\s*(?:(,))" + } + ] } ] }, @@ -4084,7 +4189,11 @@ ] }, { - "match": "(?x)\\b( accent-height | align-content | align-items | align-self | alignment-baseline | all | animation-timing-function | animation-play-state | animation-name | animation-iteration-count | animation-fill-mode | animation-duration | animation-direction | animation-delay | animation | appearance | ascent | azimuth | backface-visibility | background-size | background-repeat-y | background-repeat-x | background-repeat | background-position-y | background-position-x | background-position | background-origin | background-image | background-color | background-clip | background-blend-mode | background-attachment | background | baseline-shift | begin | bias | blend-mode | border-((top|right|bottom|left)-)?(width|style|color) | border-(top|bottom)-(right|left)-radius | border-image-(width|source|slice|repeat|outset) | border-(top|right|bottom|left|collapse|image|radius|spacing) | border | bottom | box-(align|decoration-break|direction|flex|ordinal-group|orient|pack|shadow|sizing) | break-(after|before|inside) | caption-side | clear | clip-path | clip-rule | clip | color(-(interpolation(-filters)?|profile|rendering))? | columns | column-(break-before|count|fill|gap|(rule(-(color|style|width))?)|span|width) | contain | content | counter-(increment|reset) | cursor | (c|d|f)(x|y) | direction | display | divisor | dominant-baseline | dur | elevation | empty-cells | enable-background | end | fallback | fill(-(opacity|rule))? | filter | flex(-(align|basis|direction|flow|grow|item-align|line-pack|negative|order|pack|positive|preferred-size|shrink|wrap))? | float | flood-(color|opacity) | font-display | font-family | font-feature-settings | font-kerning | font-language-override | font-size(-adjust)? | font-smoothing | font-stretch | font-style | font-synthesis | font-variant(-(alternates|caps|east-asian|ligatures|numeric|position))? | font-weight | font | fr | glyph-orientation-(horizontal|vertical) | grid-(area|gap) | grid-auto-(columns|flow|rows) | grid-(column|row)(-(end|gap|start))? | grid-template(-(areas|columns|rows))? | height | hyphens | image-(orientation|rendering|resolution) | isolation | justify-content | kerning | left | letter-spacing | lighting-color | line-(box-contain|break|clamp|height) | list-style(-(image|position|type))? | margin(-(bottom|left|right|top))? | marker(-(end|mid|start))? | mask(-(clip||composite|image|origin|position|repeat|size|type))? | (max|min)-(height|width) | mix-blend-mode | nbsp-mode | negative | object-(fit|position) | opacity | operator | order | orphans | outline(-(color|offset|style|width))? | overflow(-(scrolling|wrap|x|y))? | pad(ding(-(bottom|left|right|top))?)? | page(-break-(after|before|inside))? | paint-order | pause(-(after|before))? | perspective(-origin(-(x|y))?)? | pitch(-range)? | pointer-events | position | prefix | quotes | range | resize | right | rotate | scale | scroll-behavior | shape-(image-threshold|margin|outside|rendering) | size | speak(-as)? | src | stop-(color|opacity) | stroke(-(dash(array|offset)|line(cap|join)|miterlimit|opacity|width))? | suffix | symbols | system | tab-size | table-layout | tap-highlight-color | text-align(-last)? | text-decoration(-(color|line|style))? | text-emphasis(-(color|position|style))? | text-(anchor|fill-color|height|indent|justify|orientation|overflow|rendering|shadow|transform|underline-position) | top | touch-action | transform(-origin(-(x|y))?) | transform(-style)? | transition(-(delay|duration|property|timing-function))? | translate | unicode-(bidi|range) | user-(drag|select) | vertical-align | visibility | white-space | widows | width | will-change | word-(break|spacing|wrap) | writing-mode | z-index | zoom )\\b", + "match": "(?x)\\b( accent-height | align-content | align-items | align-self | alignment-baseline | all | animation-timing-function | animation-play-state | animation-name | animation-iteration-count | animation-fill-mode | animation-duration | animation-direction | animation-delay | animation | appearance | ascent | azimuth | backface-visibility | background-size | background-repeat-y | background-repeat-x | background-repeat | background-position-y | background-position-x | background-position | background-origin | background-image | background-color | background-clip | background-blend-mode | background-attachment | background | baseline-shift | begin | bias | blend-mode | border-((top|right|bottom|left|((block|inline)(-(start|end))?))-)?(width|style|color) | border-((top|bottom)-(right|left)|((start|end)-?){1,2})-radius | border-image-(width|source|slice|repeat|outset) | border-(top|right|bottom|left|collapse|image|radius|spacing|((block|inline)(-(start|end))?)) | border | bottom | box-(align|decoration-break|direction|flex|ordinal-group|orient|pack|shadow|sizing) | break-(after|before|inside) | caption-side | clear | clip-path | clip-rule | clip | color(-(interpolation(-filters)?|profile|rendering))? | columns | column-(break-before|count|fill|gap|(rule(-(color|style|width))?)|span|width) | contain(-intrinsic-((((block|inline)-)?size)|height|width))? | content | counter-(increment|reset) | cursor | (c|d|f)(x|y) | direction | display | divisor | dominant-baseline | dur | elevation | empty-cells | enable-background | end | fallback | fill(-(opacity|rule))? | filter | flex(-(align|basis|direction|flow|grow|item-align|line-pack|negative|order|pack|positive|preferred-size|shrink|wrap))? | float | flood-(color|opacity) | font-display | font-family | font-feature-settings | font-kerning | font-language-override | font-size(-adjust)? | font-smoothing | font-stretch | font-style | font-synthesis | font-variant(-(alternates|caps|east-asian|ligatures|numeric|position))? | font-weight | font | fr | ((column|row)-)?gap | glyph-orientation-(horizontal|vertical) | grid-(area|gap) | grid-auto-(columns|flow|rows) | grid-(column|row)(-(end|gap|start))? | grid-template(-(areas|columns|rows))? | height | hyphens | image-(orientation|rendering|resolution) | inset(-(block|inline))?(-(start|end))? | isolation | justify-content | justify-items | justify-self | kerning | left | letter-spacing | lighting-color | line-(box-contain|break|clamp|height) | list-style(-(image|position|type))? | (margin|padding)(-(bottom|left|right|top)|(-(block|inline)?(-(end|start))?))? | marker(-(end|mid|start))? | mask(-(clip||composite|image|origin|position|repeat|size|type))? | (max|min)-(height|width) | mix-blend-mode | nbsp-mode | negative | object-(fit|position) | opacity | operator | order | orphans | outline(-(color|offset|style|width))? | overflow(-((inline|block)|scrolling|wrap|x|y))? | overscroll-behavior(-block|-(inline|x|y))? | pad(ding(-(bottom|left|right|top))?)? | page(-break-(after|before|inside))? | paint-order | pause(-(after|before))? | perspective(-origin(-(x|y))?)? | pitch(-range)? | place-content | place-self | pointer-events | position | prefix | quotes | range | resize | right | rotate | scale | scroll-behavior | shape-(image-threshold|margin|outside|rendering) | size | speak(-as)? | src | stop-(color|opacity) | stroke(-(dash(array|offset)|line(cap|join)|miterlimit|opacity|width))? | suffix | symbols | system | tab-size | table-layout | tap-highlight-color | text-align(-last)? | text-decoration(-(color|line|style))? | text-emphasis(-(color|position|style))? | text-(anchor|fill-color|height|indent|justify|orientation|overflow|rendering|size-adjust|shadow|transform|underline-position|wrap) | top | touch-action | transform(-origin(-(x|y))?) | transform(-style)? | transition(-(delay|duration|property|timing-function))? | translate | unicode-(bidi|range) | user-(drag|select) | vertical-align | visibility | white-space(-collapse)? | widows | width | will-change | word-(break|spacing|wrap) | writing-mode | z-index | zoom )\\b", + "name": "support.type.property-name.less" + }, + { + "match": "(?x)\\b(((contain-intrinsic|max|min)-)?(block|inline)?-size)\\b", "name": "support.type.property-name.less" }, { @@ -4093,7 +4202,15 @@ ] }, { - "begin": "\\b(((\\+_?)?):)([\\s\\t]*)", + "begin": "\\b((?:(?:\\+_?)?):)([\\s\\t]*)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.key-value.less" + }, + "2": { + "name": "meta.property-value.less" + } + }, "captures": { "1": { "name": "punctuation.separator.key-value.less" @@ -5002,6 +5119,9 @@ }, { "include": "#less-variables" + }, + { + "include": "#property-values" } ] } diff --git a/extensions/markdown-basics/language-configuration.json b/extensions/markdown-basics/language-configuration.json index f1e7859ccca..6e1766db02c 100644 --- a/extensions/markdown-basics/language-configuration.json +++ b/extensions/markdown-basics/language-configuration.json @@ -79,6 +79,10 @@ [ "<", ">" + ], + [ + "~", + "~" ] ], "folding": { diff --git a/extensions/markdown-language-features/media/markdown.css b/extensions/markdown-language-features/media/markdown.css index 168f6a8a862..800be985a43 100644 --- a/extensions/markdown-language-features/media/markdown.css +++ b/extensions/markdown-language-features/media/markdown.css @@ -205,7 +205,7 @@ table > tbody > tr + tr > td { blockquote { margin: 0; - padding: 2px 16px 0 10px; + padding: 0px 16px 0 10px; border-left-width: 5px; border-left-style: solid; border-radius: 2px; diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 3d73a7621fd..fd3ba077028 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -14,7 +14,8 @@ ], "activationEvents": [], "enabledApiProposals": [ - "idToken" + "idToken", + "authGetSessions" ], "capabilities": { "virtualWorkspaces": true, diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index df36686dc9a..bc4d71e56d6 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -203,11 +203,13 @@ export class AzureActiveDirectoryService { return this._sessionChangeEmitter.event; } - public getSessions(scopes?: string[]): Promise { + public getSessions(scopes?: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise { if (!scopes) { this._logger.info('Getting sessions for all scopes...'); - const sessions = this._tokens.map(token => this.convertToSessionSync(token)); - this._logger.info(`Got ${sessions.length} sessions for all scopes...`); + const sessions = this._tokens + .filter(token => !account?.label || token.account.label === account.label) + .map(token => this.convertToSessionSync(token)); + this._logger.info(`Got ${sessions.length} sessions for all scopes${account ? ` for account '${account.label}'` : ''}...`); return Promise.resolve(sessions); } @@ -238,23 +240,43 @@ export class AzureActiveDirectoryService { tenant: this.getTenantId(scopes), }; - this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions`); - return this._sequencer.queue(modifiedScopesStr, () => this.doGetSessions(scopeData)); + this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions` + account ? ` for ${account?.label}` : ''); + return this._sequencer.queue(modifiedScopesStr, () => this.doGetSessions(scopeData, account)); } - private async doGetSessions(scopeData: IScopeData): Promise { - this._logger.info(`[${scopeData.scopeStr}] Getting sessions`); + private async doGetSessions(scopeData: IScopeData, account?: vscode.AuthenticationSessionAccountInformation): Promise { + this._logger.info(`[${scopeData.scopeStr}] Getting sessions` + account ? ` for ${account?.label}` : ''); - const matchingTokens = this._tokens.filter(token => token.scope === scopeData.scopeStr); + const matchingTokens = this._tokens + .filter(token => token.scope === scopeData.scopeStr) + .filter(token => !account?.label || token.account.label === account.label); // If we still don't have a matching token try to get a new token from an existing token by using // the refreshToken. This is documented here: // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token // "Refresh tokens are valid for all permissions that your client has already received consent for." if (!matchingTokens.length) { - // Get a token with the correct client id. - const token = scopeData.clientId === DEFAULT_CLIENT_ID - ? this._tokens.find(t => t.refreshToken && !t.scope.includes('VSCODE_CLIENT_ID')) - : this._tokens.find(t => t.refreshToken && t.scope.includes(`VSCODE_CLIENT_ID:${scopeData.clientId}`)); + // Get a token with the correct client id and account. + let token: IToken | undefined; + for (const t of this._tokens) { + // No refresh token, so we can't make a new token from this session + if (!t.refreshToken) { + continue; + } + // Need to make sure the account matches if we were provided one + if (account?.label && t.account.label !== account.label) { + continue; + } + // If the client id is the default client id, then check for the absence of the VSCODE_CLIENT_ID scope + if (scopeData.clientId === DEFAULT_CLIENT_ID && !t.scope.includes('VSCODE_CLIENT_ID')) { + token = t; + break; + } + // If the client id is not the default client id, then check for the matching VSCODE_CLIENT_ID scope + if (scopeData.clientId !== DEFAULT_CLIENT_ID && t.scope.includes(`VSCODE_CLIENT_ID:${scopeData.clientId}`)) { + token = t; + break; + } + } if (token) { this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Found a matching token with a different scopes '${token.scope}'. Attempting to get a new session using the existing session.`); @@ -275,7 +297,7 @@ export class AzureActiveDirectoryService { .map(result => (result as PromiseFulfilledResult).value); } - public createSession(scopes: string[]): Promise { + public createSession(scopes: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise { let modifiedScopes = [...scopes]; if (!modifiedScopes.includes('openid')) { modifiedScopes.push('openid'); @@ -301,11 +323,11 @@ export class AzureActiveDirectoryService { }; this._logger.trace(`[${scopeData.scopeStr}] Queued creating session`); - return this._sequencer.queue(scopeData.scopeStr, () => this.doCreateSession(scopeData)); + return this._sequencer.queue(scopeData.scopeStr, () => this.doCreateSession(scopeData, account)); } - private async doCreateSession(scopeData: IScopeData): Promise { - this._logger.info(`[${scopeData.scopeStr}] Creating session`); + private async doCreateSession(scopeData: IScopeData, account?: vscode.AuthenticationSessionAccountInformation): Promise { + this._logger.info(`[${scopeData.scopeStr}] Creating session` + account ? ` for ${account?.label}` : ''); const runsRemote = vscode.env.remoteName !== undefined; const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web; @@ -316,17 +338,17 @@ export class AzureActiveDirectoryService { return await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Signing in to your account...'), cancellable: true }, async (_progress, token) => { if (runsRemote || runsServerless) { - return await this.createSessionWithoutLocalServer(scopeData, token); + return await this.createSessionWithoutLocalServer(scopeData, account?.label, token); } try { - return await this.createSessionWithLocalServer(scopeData, token); + return await this.createSessionWithLocalServer(scopeData, account?.label, token); } catch (e) { this._logger.error(`[${scopeData.scopeStr}] Error creating session: ${e}`); // If the error was about starting the server, try directly hitting the login endpoint instead if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { - return this.createSessionWithoutLocalServer(scopeData, token); + return this.createSessionWithoutLocalServer(scopeData, account?.label, token); } throw e; @@ -334,7 +356,7 @@ export class AzureActiveDirectoryService { }); } - private async createSessionWithLocalServer(scopeData: IScopeData, token: vscode.CancellationToken): Promise { + private async createSessionWithLocalServer(scopeData: IScopeData, loginHint: string | undefined, token: vscode.CancellationToken): Promise { this._logger.trace(`[${scopeData.scopeStr}] Starting login flow with local server`); const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); @@ -344,11 +366,15 @@ export class AzureActiveDirectoryService { client_id: scopeData.clientId, redirect_uri: redirectUrl, scope: scopeData.scopesToSend, - prompt: 'select_account', code_challenge_method: 'S256', code_challenge: codeChallenge, - }).toString(); - const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`, this._env.activeDirectoryEndpointUrl).toString(); + }); + if (loginHint) { + qs.set('login_hint', loginHint); + } else { + qs.set('prompt', 'select_account'); + } + const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs.toString()}`, this._env.activeDirectoryEndpointUrl).toString(); const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl); await server.start(); @@ -370,7 +396,7 @@ export class AzureActiveDirectoryService { return session; } - private async createSessionWithoutLocalServer(scopeData: IScopeData, token: vscode.CancellationToken): Promise { + private async createSessionWithoutLocalServer(scopeData: IScopeData, loginHint: string | undefined, token: vscode.CancellationToken): Promise { this._logger.trace(`[${scopeData.scopeStr}] Starting login flow without local server`); let callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`)); const nonce = generateCodeVerifier(); @@ -383,17 +409,22 @@ export class AzureActiveDirectoryService { const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); const signInUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize`, this._env.activeDirectoryEndpointUrl); - signInUrl.search = new URLSearchParams({ + const qs = new URLSearchParams({ response_type: 'code', client_id: encodeURIComponent(scopeData.clientId), response_mode: 'query', redirect_uri: redirectUrl, state, scope: scopeData.scopesToSend, - prompt: 'select_account', code_challenge_method: 'S256', code_challenge: codeChallenge, - }).toString(); + }); + if (loginHint) { + qs.append('login_hint', loginHint); + } else { + qs.append('prompt', 'select_account'); + } + signInUrl.search = qs.toString(); const uri = vscode.Uri.parse(signInUrl.toString()); vscode.env.openExternal(uri); diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 02cfb4643f4..87dc94e4c25 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -123,8 +123,8 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', { onDidChangeSessions: loginService.onDidChangeSessions, - getSessions: (scopes: string[]) => loginService.getSessions(scopes), - createSession: async (scopes: string[]) => { + getSessions: (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => loginService.getSessions(scopes, options?.account), + createSession: async (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => { try { /* __GDPR__ "login" : { @@ -138,7 +138,7 @@ export async function activate(context: vscode.ExtensionContext) { scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), }); - return await loginService.createSession(scopes); + return await loginService.createSession(scopes, options?.account); } catch (e) { /* __GDPR__ "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } diff --git a/extensions/microsoft-authentication/tsconfig.json b/extensions/microsoft-authentication/tsconfig.json index 4b9d06d1847..cad76d078bd 100644 --- a/extensions/microsoft-authentication/tsconfig.json +++ b/extensions/microsoft-authentication/tsconfig.json @@ -22,6 +22,7 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.idToken.d.ts" + "../../src/vscode-dts/vscode.proposed.idToken.d.ts", + "../../src/vscode-dts/vscode.proposed.authGetSessions.d.ts" ] } diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index 8954017fffc..8f5fa908cb9 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -11,7 +11,7 @@ import { formatStackTrace } from './stackTraceHelper'; function clearContainer(container: HTMLElement) { while (container.firstChild) { - container.removeChild(container.firstChild); + container.firstChild.remove(); } } @@ -378,7 +378,7 @@ function renderStream(outputInfo: OutputWithAppend, outputElement: HTMLElement, contentParent = document.createElement('div'); contentParent.appendChild(newContent); while (outputElement.firstChild) { - outputElement.removeChild(outputElement.firstChild); + outputElement.firstChild.remove(); } outputElement.appendChild(contentParent); } @@ -462,7 +462,7 @@ export const activate: ActivationFunction = (ctx) => { border-color: var(--theme-input-focus-border-color); } #container div.output .scrollable { - overflow-y: scroll; + overflow-y: auto; max-height: var(--notebook-cell-output-max-height); } #container div.output .scrollable.scrollbar-visible { diff --git a/extensions/notebook-renderers/src/textHelper.ts b/extensions/notebook-renderers/src/textHelper.ts index b49dbb6ad8d..9c080c7f9e4 100644 --- a/extensions/notebook-renderers/src/textHelper.ts +++ b/extensions/notebook-renderers/src/textHelper.ts @@ -71,6 +71,11 @@ function generateNestedViewAllElement(outputId: string) { function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number, linkOptions: LinkOptions) { const container = document.createElement('div'); + container.setAttribute('data-vscode-context', JSON.stringify({ + webviewSection: 'text', + outputId: id, + 'preventDefaultContextMenuItems': true + })); const lineCount = buffer.length; if (lineCount <= linesLimit) { @@ -95,6 +100,11 @@ function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number function scrollableArrayOfString(id: string, buffer: string[], linkOptions: LinkOptions) { const element = document.createElement('div'); + element.setAttribute('data-vscode-context', JSON.stringify({ + webviewSection: 'text', + outputId: id, + 'preventDefaultContextMenuItems': true + })); if (buffer.length > softScrollableLineLimit) { element.appendChild(generateNestedViewAllElement(id)); } diff --git a/extensions/notebook-renderers/yarn.lock b/extensions/notebook-renderers/yarn.lock index 3cbe531e0fd..00c3e704dba 100644 --- a/extensions/notebook-renderers/yarn.lock +++ b/extensions/notebook-renderers/yarn.lock @@ -408,9 +408,9 @@ word-wrap@~1.2.3: integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== ws@^8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xml-name-validator@^4.0.0: version "4.0.0" diff --git a/extensions/npm/yarn.lock b/extensions/npm/yarn.lock index a7afc9f801f..be4b192c67d 100644 --- a/extensions/npm/yarn.lock +++ b/extensions/npm/yarn.lock @@ -39,21 +39,21 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" diff --git a/extensions/package.json b/extensions/package.json index 2c83af40936..940bbe9b8a2 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "5.4.5" + "typescript": "^5.5.2" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/extensions/swift/cgmanifest.json b/extensions/swift/cgmanifest.json index 816621e4170..cb1ca02310f 100644 --- a/extensions/swift/cgmanifest.json +++ b/extensions/swift/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "jtbandes/swift-tmlanguage", "repositoryUrl": "https://github.com/jtbandes/swift-tmlanguage", - "commitHash": "ab893c684dd7eeb7c249139e29e931334316fda7" + "commitHash": "860eface4241cf9f2174d5fa690bd34389ac8d26" } }, "license": "MIT" diff --git a/extensions/swift/syntaxes/swift.tmLanguage.json b/extensions/swift/syntaxes/swift.tmLanguage.json index 6259b151369..b18b340f2c6 100644 --- a/extensions/swift/syntaxes/swift.tmLanguage.json +++ b/extensions/swift/syntaxes/swift.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jtbandes/swift-tmlanguage/commit/ab893c684dd7eeb7c249139e29e931334316fda7", + "version": "https://github.com/jtbandes/swift-tmlanguage/commit/860eface4241cf9f2174d5fa690bd34389ac8d26", "name": "Swift", "scopeName": "source.swift", "comment": "See swift.tmbundle/grammar-test.swift for test cases.", @@ -52,7 +52,7 @@ }, "patterns": [ { - "match": "\\b(swift|(?:iOS|macOS|OSX|watchOS|tvOS|UIKitForMac)(?:ApplicationExtension)?)\\b(?:\\s+([0-9]+(?:\\.[0-9]+)*\\b))?", + "match": "\\b(swift|(?:iOS|macOS|OSX|watchOS|tvOS|visionOS|UIKitForMac)(?:ApplicationExtension)?)\\b(?:\\s+([0-9]+(?:\\.[0-9]+)*\\b))?", "captures": { "1": { "name": "keyword.other.platform.os.swift" @@ -580,7 +580,7 @@ } }, { - "match": "\\b(os)\\s*(\\()\\s*(?:(macOS|OSX|iOS|tvOS|watchOS|Android|Linux|FreeBSD|Windows|PS4)|\\w+)\\s*(\\))", + "match": "\\b(os)\\s*(\\()\\s*(?:(macOS|OSX|iOS|tvOS|watchOS|visionOS|Android|Linux|FreeBSD|Windows|PS4)|\\w+)\\s*(\\))", "captures": { "1": { "name": "keyword.other.condition.swift" @@ -2586,7 +2586,7 @@ }, "patterns": [ { - "match": "\\s*\\b((?:iOS|macOS|OSX|watchOS|tvOS|UIKitForMac)(?:ApplicationExtension)?)\\b(?:\\s+([0-9]+(?:\\.[0-9]+)*\\b))", + "match": "\\s*\\b((?:iOS|macOS|OSX|watchOS|tvOS|visionOS|UIKitForMac)(?:ApplicationExtension)?)\\b(?:\\s+([0-9]+(?:\\.[0-9]+)*\\b))", "captures": { "1": { "name": "keyword.other.platform.os.swift" diff --git a/extensions/tunnel-forwarding/src/extension.ts b/extensions/tunnel-forwarding/src/extension.ts index 3dc88224aaa..299c728719f 100644 --- a/extensions/tunnel-forwarding/src/extension.ts +++ b/extensions/tunnel-forwarding/src/extension.ts @@ -37,6 +37,7 @@ class Tunnel implements vscode.Tunnel { constructor( public readonly remoteAddress: { port: number; host: string }, public readonly privacy: TunnelPrivacyId, + public readonly protocol: 'http' | 'https', ) { } public setPortFormat(formatString: string) { @@ -82,7 +83,7 @@ export async function activate(context: vscode.ExtensionContext) { { tunnelFeatures: { elevation: false, - protocol: false, + protocol: true, privacyOptions: [ { themeIcon: 'globe', id: TunnelPrivacyId.Public, label: vscode.l10n.t('Public') }, { themeIcon: 'lock', id: TunnelPrivacyId.Private, label: vscode.l10n.t('Private') }, @@ -152,6 +153,7 @@ class TunnelProvider implements vscode.TunnelProvider { const tunnel = new Tunnel( tunnelOptions.remoteAddress, (tunnelOptions.privacy as TunnelPrivacyId) || TunnelPrivacyId.Private, + tunnelOptions.protocol === 'https' ? 'https' : 'http', ); this.tunnels.add(tunnel); @@ -238,7 +240,7 @@ class TunnelProvider implements vscode.TunnelProvider { return; } - const ports = [...this.tunnels].map(t => ({ number: t.remoteAddress.port, privacy: t.privacy })); + const ports = [...this.tunnels].map(t => ({ number: t.remoteAddress.port, privacy: t.privacy, protocol: t.protocol })); this.state.process.stdin.write(`${JSON.stringify(ports)}\n`); if (ports.length === 0 && !this.state.cleanupTimeout) { diff --git a/extensions/typescript-basics/language-configuration.json b/extensions/typescript-basics/language-configuration.json index 070b8911a82..25a23685738 100644 --- a/extensions/typescript-basics/language-configuration.json +++ b/extensions/typescript-basics/language-configuration.json @@ -129,10 +129,10 @@ }, "indentationRules": { "decreaseIndentPattern": { - "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]\\)].*$" + "pattern": "^\\s*[\\}\\]\\)].*$" }, "increaseIndentPattern": { - "pattern": "^((?!//).)*(\\{([^}\"'`/]*|(\\t|[ ])*//.*)|\\([^)\"'`/]*|\\[[^\\]\"'`/]*)$" + "pattern": "^.*(\\{[^}]*|\\([^)]*|\\[[^\\]]*)$" }, // e.g. * ...| or */| or *-----*/| "unIndentedLinePattern": { diff --git a/extensions/typescript-basics/snippets/typescript.code-snippets b/extensions/typescript-basics/snippets/typescript.code-snippets index 35b2aa1711c..9ed695795eb 100644 --- a/extensions/typescript-basics/snippets/typescript.code-snippets +++ b/extensions/typescript-basics/snippets/typescript.code-snippets @@ -163,7 +163,7 @@ "For-Of Loop": { "prefix": "forof", "body": [ - "for (const ${1:iterator} of ${2:object}) {", + "for (const ${1:element} of ${2:object}) {", "\t$TM_SELECTED_TEXT$0", "}" ], @@ -172,7 +172,7 @@ "For-Await-Of Loop": { "prefix": "forawaitof", "body": [ - "for await (const ${1:iterator} of ${2:object}) {", + "for await (const ${1:element} of ${2:object}) {", "\t$TM_SELECTED_TEXT$0", "}" ], @@ -266,6 +266,15 @@ ], "description": "Set Timeout Function" }, + "Set Interval Function": { + "prefix": "setinterval", + "body": [ + "setInterval(() => {", + "\t$TM_SELECTED_TEXT$0", + "}, ${1:interval});" + ], + "description": "Set Interval Function" + }, "Region Start": { "prefix": "#region", "body": [ diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index e01c9c96527..3f7fdcf252f 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -13,7 +13,8 @@ "multiDocumentHighlightProvider", "mappedEditsProvider", "codeActionAI", - "codeActionRanges" + "codeActionRanges", + "documentPaste" ], "capabilities": { "virtualWorkspaces": { @@ -1176,10 +1177,7 @@ "typescript.tsserver.experimental.useVsCodeWatcher": { "type": "boolean", "description": "%configuration.tsserver.useVsCodeWatcher%", - "default": false, - "tags": [ - "experimental" - ] + "default": true }, "typescript.tsserver.watchOptions": { "type": "object", @@ -1284,13 +1282,13 @@ }, "typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors": { "type": "boolean", - "default": false, + "default": true, "description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%", "scope": "window" }, "typescript.tsserver.web.typeAcquisition.enabled": { "type": "boolean", - "default": true, + "default": false, "description": "%configuration.tsserver.web.typeAcquisition.enabled%", "scope": "window" }, @@ -1316,6 +1314,26 @@ "default": true, "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", "scope": "window" + }, + "typescript.tsserver.enableRegionDiagnostics": { + "type": "boolean", + "default": true, + "description": "%typescript.tsserver.enableRegionDiagnostics%", + "scope": "window" + }, + "javascript.experimental.updateImportsOnPaste": { + "scope": "window", + "type": "boolean", + "default": false, + "description": "%configuration.updateImportsOnPaste%", + "tags": ["experimental"] + }, + "typescript.experimental.updateImportsOnPaste": { + "scope": "window", + "type": "boolean", + "default": false, + "description": "%configuration.updateImportsOnPaste%", + "tags": ["experimental"] } } }, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 7fb5bae6ad1..cba24007314 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -16,6 +16,7 @@ "typescript.tsserver.pluginPaths": "Additional paths to discover TypeScript Language Service plugins.", "typescript.tsserver.pluginPaths.item": "Either an absolute or relative path. Relative path will be resolved against workspace folder(s).", "typescript.tsserver.trace": "Enables tracing of messages sent to the TS server. This trace can be used to diagnose TS Server issues. The trace may contain file paths, source code, and other potentially sensitive information from your project.", + "typescript.tsserver.enableRegionDiagnostics": "Enables region-based diagnostics in TypeScript. Requires using TypeScript 5.6+ in the workspace.", "typescript.validate.enable": "Enable/disable TypeScript validation.", "typescript.format.enable": "Enable/disable default TypeScript formatter.", "javascript.format.enable": "Enable/disable default JavaScript formatter.", @@ -219,6 +220,7 @@ "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors on web even when project wide IntelliSense is enabled. This is always on when project wide IntelliSense is not enabled or available. See `#typescript.tsserver.web.projectWideIntellisense.enabled#`", "configuration.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web. This enables IntelliSense for imported packages. Requires `#typescript.tsserver.web.projectWideIntellisense.enabled#`. Currently not supported for Safari.", "configuration.tsserver.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.", + "configuration.updateImportsOnPaste": "Automatically update imports when pasting code. Requires TypeScript 5.6+.", "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", "walkthroughs.nodejsWelcome.downloadNode.forMacOrWindows.title": "Install Node.js", diff --git a/extensions/typescript-language-features/src/configuration/configuration.ts b/extensions/typescript-language-features/src/configuration/configuration.ts index 2f0ff4b0a28..639f3d346e0 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -117,13 +117,14 @@ export interface TypeScriptServiceConfiguration { readonly enableProjectDiagnostics: boolean; readonly maxTsServerMemory: number; readonly enablePromptUseWorkspaceTsdk: boolean; - readonly useVsCodeWatcher: boolean; + readonly useVsCodeWatcher: boolean; // TODO@bpasero remove this setting eventually readonly watchOptions: Proto.WatchOptions | undefined; readonly includePackageJsonAutoImports: 'auto' | 'on' | 'off' | undefined; readonly enableTsServerTracing: boolean; readonly localNodePath: string | null; readonly globalNodePath: string | null; readonly workspaceSymbolsExcludeLibrarySymbols: boolean; + readonly enableRegionDiagnostics: boolean; } export function areServiceConfigurationsEqual(a: TypeScriptServiceConfiguration, b: TypeScriptServiceConfiguration): boolean { @@ -162,6 +163,7 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu localNodePath: this.readLocalNodePath(configuration), globalNodePath: this.readGlobalNodePath(configuration), workspaceSymbolsExcludeLibrarySymbols: this.readWorkspaceSymbolsExcludeLibrarySymbols(configuration), + enableRegionDiagnostics: this.readEnableRegionDiagnostics(configuration), }; } @@ -261,10 +263,14 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu } private readWebProjectWideIntellisenseSuppressSemanticErrors(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors', false); + return configuration.get('typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors', true); } private readWebTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.tsserver.web.typeAcquisition.enabled', true); + return configuration.get('typescript.tsserver.web.typeAcquisition.enabled', false); + } + + private readEnableRegionDiagnostics(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.tsserver.enableRegionDiagnostics', true); } } diff --git a/extensions/typescript-language-features/src/configuration/fileSchemes.ts b/extensions/typescript-language-features/src/configuration/fileSchemes.ts index cfc3f66db17..ca268e29e0b 100644 --- a/extensions/typescript-language-features/src/configuration/fileSchemes.ts +++ b/extensions/typescript-language-features/src/configuration/fileSchemes.ts @@ -17,8 +17,13 @@ export const vsls = 'vsls'; export const walkThroughSnippet = 'walkThroughSnippet'; export const vscodeNotebookCell = 'vscode-notebook-cell'; export const officeScript = 'office-script'; + +/** Used for code blocks in chat by vs code core */ export const chatCodeBlock = 'vscode-chat-code-block'; +/** Used for code blocks in chat by copilot. */ +export const chatBackingCodeBlock = 'vscode-copilot-chat-code-block'; + export function getSemanticSupportedSchemes() { if (isWeb() && vscode.workspace.workspaceFolders) { return vscode.workspace.workspaceFolders.map(folder => folder.uri.scheme); @@ -30,6 +35,7 @@ export function getSemanticSupportedSchemes() { walkThroughSnippet, vscodeNotebookCell, chatCodeBlock, + chatBackingCodeBlock, ]; } @@ -42,3 +48,8 @@ export const disabledSchemes = new Set([ github, azurerepos, ]); + +export function isOfScheme(uri: vscode.Uri, ...schemes: string[]): boolean { + const normalizedUriScheme = uri.scheme.toLowerCase(); + return schemes.some(scheme => normalizedUriScheme === scheme); +} diff --git a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts new file mode 100644 index 00000000000..83a7bb38639 --- /dev/null +++ b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { DocumentSelector } from '../configuration/documentSelector'; +import * as typeConverters from '../typeConverters'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { conditionalRegistration, requireGlobalConfiguration, requireMinVersion, requireSomeCapability } from './util/dependentRegistration'; +import protocol from '../tsServer/protocol/protocol'; +import { API } from '../tsServer/api'; +import { LanguageDescription } from '../configuration/languageDescription'; + +class CopyMetadata { + constructor( + readonly resource: vscode.Uri, + readonly ranges: readonly vscode.Range[], + ) { } + + toJSON() { + return JSON.stringify({ + resource: this.resource.toJSON(), + ranges: this.ranges, + }); + } + + static fromJSON(str: string): CopyMetadata | undefined { + try { + const parsed = JSON.parse(str); + return new CopyMetadata( + vscode.Uri.from(parsed.resource), + parsed.ranges.map((r: any) => new vscode.Range(r[0].line, r[0].character, r[1].line, r[1].character))); + } catch { + // ignore + } + return undefined; + } +} + +const settingId = 'experimental.updateImportsOnPaste'; + +class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { + + static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('text', 'jsts', 'pasteWithImports'); + static readonly metadataMimeType = 'application/vnd.code.jsts.metadata'; + + constructor( + private readonly _modeId: string, + private readonly _client: ITypeScriptServiceClient, + ) { } + + prepareDocumentPaste(document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken) { + dataTransfer.set(DocumentPasteProvider.metadataMimeType, + new vscode.DataTransferItem(new CopyMetadata(document.uri, ranges).toJSON())); + } + + async provideDocumentPasteEdits( + document: vscode.TextDocument, + ranges: readonly vscode.Range[], + dataTransfer: vscode.DataTransfer, + _context: vscode.DocumentPasteEditContext, + token: vscode.CancellationToken, + ): Promise { + const config = vscode.workspace.getConfiguration(this._modeId, document.uri); + if (!config.get(settingId, false)) { + return; + } + + const file = this._client.toOpenTsFilePath(document); + if (!file) { + return; + } + + const text = await dataTransfer.get('text/plain')?.asString(); + if (!text || token.isCancellationRequested) { + return; + } + + // Get optional metadata + const metadata = await this.extractMetadata(dataTransfer, token); + if (token.isCancellationRequested) { + return; + } + + let copiedFrom: { + file: string; + spans: protocol.TextSpan[]; + } | undefined; + if (metadata) { + const spans = metadata.ranges.map(typeConverters.Range.toTextSpan); + const copyFile = this._client.toTsFilePath(metadata.resource); + if (copyFile) { + copiedFrom = { file: copyFile, spans }; + } + } + + if (copiedFrom?.file === file) { + return; + } + + const response = await this._client.interruptGetErr(() => this._client.execute('getPasteEdits', { + file, + // TODO: only supports a single paste for now + pastedText: [text], + pasteLocations: ranges.map(typeConverters.Range.toTextSpan), + copiedFrom + }, token)); + if (response.type !== 'response' || !response.body || token.isCancellationRequested) { + return; + } + + const edit = new vscode.DocumentPasteEdit('', vscode.l10n.t("Paste with imports"), DocumentPasteProvider.kind); + const additionalEdit = new vscode.WorkspaceEdit(); + for (const edit of response.body.edits) { + additionalEdit.set(this._client.toResource(edit.fileName), edit.textChanges.map(typeConverters.TextEdit.fromCodeEdit)); + } + edit.additionalEdit = additionalEdit; + return [edit]; + } + + private async extractMetadata(dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { + const metadata = await dataTransfer.get(DocumentPasteProvider.metadataMimeType)?.asString(); + if (token.isCancellationRequested) { + return undefined; + } + + return metadata ? CopyMetadata.fromJSON(metadata) : undefined; + } +} + +export function register(selector: DocumentSelector, language: LanguageDescription, client: ITypeScriptServiceClient) { + return conditionalRegistration([ + requireSomeCapability(client, ClientCapability.Semantic), + requireMinVersion(client, API.v560), + requireGlobalConfiguration(language.id, settingId), + ], () => { + return vscode.languages.registerDocumentPasteEditProvider(selector.semantic, new DocumentPasteProvider(language.id, client), { + providedPasteEditKinds: [DocumentPasteProvider.kind], + copyMimeTypes: [DocumentPasteProvider.metadataMimeType], + pasteMimeTypes: ['text/plain'], + }); + }); +} diff --git a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts index 190e6a99bf7..990aefdfa56 100644 --- a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts @@ -34,6 +34,7 @@ export const enum DiagnosticKind { Syntax, Semantic, Suggestion, + RegionSemantic, } class FileDiagnostics { @@ -48,7 +49,8 @@ class FileDiagnostics { public updateDiagnostics( language: DiagnosticLanguage, kind: DiagnosticKind, - diagnostics: ReadonlyArray + diagnostics: ReadonlyArray, + ranges: ReadonlyArray | undefined ): boolean { if (language !== this.language) { this._diagnostics.clear(); @@ -61,6 +63,9 @@ class FileDiagnostics { return false; } + if (kind === DiagnosticKind.RegionSemantic) { + return this.updateRegionDiagnostics(diagnostics, ranges!); + } this._diagnostics.set(kind, diagnostics); return true; } @@ -83,6 +88,23 @@ class FileDiagnostics { } } + /** + * @param ranges The ranges whose diagnostics were updated. + */ + private updateRegionDiagnostics( + diagnostics: ReadonlyArray, + ranges: ReadonlyArray): boolean { + if (!this._diagnostics.get(DiagnosticKind.Semantic)) { + this._diagnostics.set(DiagnosticKind.Semantic, diagnostics); + return true; + } + const oldDiagnostics = this._diagnostics.get(DiagnosticKind.Semantic)!; + const newDiagnostics = oldDiagnostics.filter(diag => !ranges.some(range => diag.range.intersection(range))); + newDiagnostics.push(...diagnostics); + this._diagnostics.set(DiagnosticKind.Semantic, newDiagnostics); + return true; + } + private getSuggestionDiagnostics(settings: DiagnosticSettings) { const enableSuggestions = settings.getEnableSuggestions(this.language); return this.get(DiagnosticKind.Suggestion).filter(x => { @@ -284,15 +306,16 @@ export class DiagnosticsManager extends Disposable { file: vscode.Uri, language: DiagnosticLanguage, kind: DiagnosticKind, - diagnostics: ReadonlyArray + diagnostics: ReadonlyArray, + ranges: ReadonlyArray | undefined, ): void { let didUpdate = false; const entry = this._diagnostics.get(file); if (entry) { - didUpdate = entry.updateDiagnostics(language, kind, diagnostics); + didUpdate = entry.updateDiagnostics(language, kind, diagnostics, ranges); } else if (diagnostics.length) { const fileDiagnostics = new FileDiagnostics(file, language); - fileDiagnostics.updateDiagnostics(language, kind, diagnostics); + fileDiagnostics.updateDiagnostics(language, kind, diagnostics, ranges); this._diagnostics.set(file, fileDiagnostics); didUpdate = true; } diff --git a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index f96f89452b9..bddd062b3e8 100644 --- a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -191,7 +191,6 @@ export default class FileConfigurationManager extends Disposable { includeCompletionsWithClassMemberSnippets: config.get('suggest.classMemberSnippets.enabled', true), includeCompletionsWithObjectLiteralMethodSnippets: config.get('suggest.objectLiteralMethodSnippets.enabled', true), autoImportFileExcludePatterns: this.getAutoImportFileExcludePatternsPreference(preferencesConfig, vscode.workspace.getWorkspaceFolder(document.uri)?.uri), - // @ts-expect-error until 5.3 #56090 preferTypeOnlyAutoImports: preferencesConfig.get('preferTypeOnlyAutoImports', false), useLabelDetailsInCompletionEntries: true, allowIncompleteCompletions: true, diff --git a/extensions/typescript-language-features/src/languageFeatures/refactor.ts b/extensions/typescript-language-features/src/languageFeatures/refactor.ts index 8dc06235753..0364b7fad3e 100644 --- a/extensions/typescript-language-features/src/languageFeatures/refactor.ts +++ b/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -541,11 +541,12 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider { + const response = await this.interruptGetErrIfNeeded(context, () => { const file = this.client.toOpenTsFilePath(document); if (!file) { return undefined; } + this.formattingOptionsManager.ensureConfigurationForDocument(document, token); const args: Proto.GetApplicableRefactorsRequestArgs = { @@ -595,6 +596,17 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider(context: vscode.CodeActionContext, f: () => R): R { + // Only interrupt diagnostics computation when code actions are explicitly + // (such as using the refactor command or a keybinding). This is a clear + // user action so we want to return results as quickly as possible. + if (context.triggerKind === vscode.CodeActionTriggerKind.Invoke) { + return this.client.interruptGetErr(f); + } else { + return f(); + } + } + public async resolveCodeAction( codeAction: TsCodeAction, token: vscode.CancellationToken, diff --git a/extensions/typescript-language-features/src/languageFeatures/util/copilot.ts b/extensions/typescript-language-features/src/languageFeatures/util/copilot.ts index e03358a81b0..416ed00bedb 100644 --- a/extensions/typescript-language-features/src/languageFeatures/util/copilot.ts +++ b/extensions/typescript-language-features/src/languageFeatures/util/copilot.ts @@ -69,8 +69,10 @@ export class EditorChatFollowUp implements Command { expand.action.changes.flatMap((c) => c.textChanges) ) : expand.range; + const initialSelection = initialRange ? new vscode.Selection(initialRange.start, initialRange.end) : undefined; await vscode.commands.executeCommand('vscode.editorChat.start', { initialRange, + initialSelection, message, autoSend: true, }); diff --git a/extensions/typescript-language-features/src/languageFeatures/workspaceSymbols.ts b/extensions/typescript-language-features/src/languageFeatures/workspaceSymbols.ts index 98995cc6baa..3240c524781 100644 --- a/extensions/typescript-language-features/src/languageFeatures/workspaceSymbols.ts +++ b/extensions/typescript-language-features/src/languageFeatures/workspaceSymbols.ts @@ -94,7 +94,7 @@ class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvide } const uri = this.client.toResource(item.file); - if (uri.scheme === fileSchemes.chatCodeBlock) { + if (fileSchemes.isOfScheme(uri, fileSchemes.chatCodeBlock, fileSchemes.chatBackingCodeBlock)) { return; } diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index b09df40561b..7b95591604b 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -65,6 +65,7 @@ export default class LanguageProvider extends Disposable { import('./languageFeatures/codeLens/implementationsCodeLens').then(provider => this._register(provider.register(selector, this.description, this.client, cachedNavTreeResponse))), import('./languageFeatures/codeLens/referencesCodeLens').then(provider => this._register(provider.register(selector, this.description, this.client, cachedNavTreeResponse))), import('./languageFeatures/completions').then(provider => this._register(provider.register(selector, this.description, this.client, this.typingsStatus, this.fileConfigurationManager, this.commandManager, this.telemetryReporter, this.onCompletionAccepted))), + import('./languageFeatures/copyPaste').then(provider => this._register(provider.register(selector, this.description, this.client))), import('./languageFeatures/definitions').then(provider => this._register(provider.register(selector, this.client))), import('./languageFeatures/directiveCommentCompletions').then(provider => this._register(provider.register(selector, this.client))), import('./languageFeatures/documentHighlight').then(provider => this._register(provider.register(selector, this.client))), @@ -137,7 +138,11 @@ export default class LanguageProvider extends Disposable { this.client.bufferSyncSupport.requestAllDiagnostics(); } - public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: vscode.Uri, diagnostics: (vscode.Diagnostic & { reportUnnecessary: any; reportDeprecated: any })[]): void { + public diagnosticsReceived( + diagnosticsKind: DiagnosticKind, + file: vscode.Uri, + diagnostics: (vscode.Diagnostic & { reportUnnecessary: any; reportDeprecated: any })[], + ranges: vscode.Range[] | undefined): void { if (diagnosticsKind !== DiagnosticKind.Syntax && !this.client.hasCapabilityForResource(file, ClientCapability.Semantic)) { return; } @@ -174,7 +179,7 @@ export default class LanguageProvider extends Disposable { } } return true; - })); + }), ranges); } public configFileDiagnosticsReceived(file: vscode.Uri, diagnostics: vscode.Diagnostic[]): void { diff --git a/extensions/typescript-language-features/src/tsServer/api.ts b/extensions/typescript-language-features/src/tsServer/api.ts index 2378bfb53f0..4beb29d1b2b 100644 --- a/extensions/typescript-language-features/src/tsServer/api.ts +++ b/extensions/typescript-language-features/src/tsServer/api.ts @@ -37,6 +37,8 @@ export class API { public static readonly v520 = API.fromSimpleString('5.2.0'); public static readonly v544 = API.fromSimpleString('5.4.4'); public static readonly v540 = API.fromSimpleString('5.4.0'); + public static readonly v550 = API.fromSimpleString('5.5.0'); + public static readonly v560 = API.fromSimpleString('5.6.0'); public static fromVersionString(versionString: string): API { let version = semver.valid(versionString); diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index 9f5d76f5ac3..32707f1c049 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -227,7 +227,7 @@ class SyncedBuffer { return tsRoot?.startsWith(inMemoryResourcePrefix) ? undefined : tsRoot; } - return resource.scheme === fileSchemes.officeScript || resource.scheme === fileSchemes.chatCodeBlock ? '/' : undefined; + return fileSchemes.isOfScheme(resource, fileSchemes.officeScript, fileSchemes.chatCodeBlock, fileSchemes.chatBackingCodeBlock) ? '/' : undefined; } public get resource(): vscode.Uri { @@ -275,12 +275,12 @@ class SyncedBufferMap extends ResourceMap { } class PendingDiagnostics extends ResourceMap { - public getOrderedFileSet(): ResourceMap { + public getOrderedFileSet(): ResourceMap { const orderedResources = Array.from(this.entries()) .sort((a, b) => a.value - b.value) .map(entry => entry.resource); - const map = new ResourceMap(this._normalizePath, this.config); + const map = new ResourceMap(this._normalizePath, this.config); for (const resource of orderedResources) { map.set(resource, undefined); } @@ -292,7 +292,7 @@ class GetErrRequest { public static executeGetErrRequest( client: ITypeScriptServiceClient, - files: ResourceMap, + files: ResourceMap, onDone: () => void ) { return new GetErrRequest(client, files, onDone); @@ -303,7 +303,7 @@ class GetErrRequest { private constructor( private readonly client: ITypeScriptServiceClient, - public readonly files: ResourceMap, + public readonly files: ResourceMap, onDone: () => void ) { if (!this.isErrorReportingEnabled()) { @@ -313,19 +313,39 @@ class GetErrRequest { } const supportsSyntaxGetErr = this.client.apiVersion.gte(API.v440); - const allFiles = coalesce(Array.from(files.entries()) - .filter(entry => supportsSyntaxGetErr || client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic)) + const fileEntries = Array.from(files.entries()).filter(entry => supportsSyntaxGetErr || client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic)); + const allFiles = coalesce(fileEntries .map(entry => client.toTsFilePath(entry.resource))); if (!allFiles.length) { this._done = true; setImmediate(onDone); } else { - const request = this.areProjectDiagnosticsEnabled() + let request; + if (this.areProjectDiagnosticsEnabled()) { // Note that geterrForProject is almost certainly not the api we want here as it ends up computing far // too many diagnostics - ? client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token) - : client.executeAsync('geterr', { delay: 0, files: allFiles }, this._token.token); + request = client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token); + } + else { + let requestFiles; + if (this.areRegionDiagnosticsEnabled()) { + requestFiles = coalesce(fileEntries + .map(entry => { + const file = client.toTsFilePath(entry.resource); + const ranges = entry.value; + if (file && ranges) { + return typeConverters.Range.toFileRangesRequestArgs(file, ranges); + } + + return file; + })); + } + else { + requestFiles = allFiles; + } + request = client.executeAsync('geterr', { delay: 0, files: requestFiles }, this._token.token); + } request.finally(() => { if (this._done) { @@ -350,6 +370,10 @@ class GetErrRequest { return this.client.configuration.enableProjectDiagnostics && this.client.capabilities.has(ClientCapability.Semantic); } + private areRegionDiagnosticsEnabled() { + return this.client.configuration.enableRegionDiagnostics && this.client.apiVersion.gte(API.v560); + } + public cancel(): any { if (!this._done) { this._token.cancel(); @@ -722,7 +746,9 @@ export default class BufferSyncSupport extends Disposable { // Add all open TS buffers to the geterr request. They might be visible for (const buffer of this.syncedBuffers.values()) { - orderedFileSet.set(buffer.resource, undefined); + const editors = vscode.window.visibleTextEditors.filter(editor => editor.document.uri.toString() === buffer.resource.toString()); + const visibleRanges = editors.flatMap(editor => editor.visibleRanges); + orderedFileSet.set(buffer.resource, visibleRanges.length ? visibleRanges : undefined); } for (const { resource } of orderedFileSet.entries()) { @@ -752,7 +778,7 @@ export default class BufferSyncSupport extends Disposable { } private shouldValidate(buffer: SyncedBuffer): boolean { - if (buffer.resource.scheme === fileSchemes.chatCodeBlock) { + if (fileSchemes.isOfScheme(buffer.resource, fileSchemes.chatCodeBlock, fileSchemes.chatBackingCodeBlock)) { return false; } diff --git a/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts b/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts index 4f02ed29427..f1b0cca26a4 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.const.ts @@ -78,6 +78,7 @@ export enum EventName { syntaxDiag = 'syntaxDiag', semanticDiag = 'semanticDiag', suggestionDiag = 'suggestionDiag', + regionSemanticDiag = 'regionSemanticDiag', configFileDiag = 'configFileDiag', telemetry = 'telemetry', projectLanguageServiceState = 'projectLanguageServiceState', diff --git a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts index aa9b0589d2d..747e7c22e37 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts @@ -19,37 +19,5 @@ declare module '../../../../node_modules/typescript/lib/typescript' { interface Response { readonly _serverType?: ServerType; } - - export interface MapCodeRequestArgs extends FileRequestArgs { - /** - * The files and changes to try and apply/map. - */ - mapping: MapCodeRequestDocumentMapping; - } - - export interface MapCodeRequestDocumentMapping { - /** - * The specific code to map/insert/replace in the file. - */ - contents: string[]; - - /** - * Areas of "focus" to inform the code mapper with. For example, cursor - * location, current selection, viewport, etc. Nested arrays denote - * priority: toplevel arrays are more important than inner arrays, and - * inner array priorities are based on items within that array. Items - * earlier in the arrays have higher priority. - */ - focusLocations?: TextSpan[][]; - } - - export interface MapCodeRequest extends FileRequest { - command: 'mapCode'; - arguments: MapCodeRequestArgs; - } - - export interface MapCodeResponse extends Response { - body: readonly FileCodeEdits[]; - } } } diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index d03c7ea5df0..7dbde90f792 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -278,8 +278,7 @@ export class ElectronServiceProcessFactory implements TsServerProcessFactory { } const childProcess = execPath ? - child_process.spawn(JSON.stringify(execPath), [...execArgv, tsServerPath, ...runtimeArgs], { - shell: true, + child_process.spawn(execPath, [...execArgv, tsServerPath, ...runtimeArgs], { windowsHide: true, cwd: undefined, env, diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index 543140dbab5..364c0f07dae 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -234,7 +234,7 @@ export class TypeScriptServerSpawner { tsServerLog = { type: 'file', uri: logFilePath }; args.push('--logVerbosity', TsServerLogLevel.toString(configuration.tsServerLogLevel)); - args.push('--logFile', `"${logFilePath.fsPath}"`); + args.push('--logFile', logFilePath.fsPath); } } } diff --git a/extensions/typescript-language-features/src/typeConverters.ts b/extensions/typescript-language-features/src/typeConverters.ts index 58babe2bda3..067a1ff3c0a 100644 --- a/extensions/typescript-language-features/src/typeConverters.ts +++ b/extensions/typescript-language-features/src/typeConverters.ts @@ -26,14 +26,24 @@ export namespace Range { Math.max(0, start.line - 1), Math.max(start.offset - 1, 0), Math.max(0, end.line - 1), Math.max(0, end.offset - 1)); - export const toFileRangeRequestArgs = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({ - file, + // @ts-expect-error until ts 5.6 + export const toFileRange = (range: vscode.Range): Proto.FileRange => ({ startLine: range.start.line + 1, startOffset: range.start.character + 1, endLine: range.end.line + 1, endOffset: range.end.character + 1 }); + export const toFileRangeRequestArgs = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({ + file, + ...toFileRange(range) + }); + // @ts-expect-error until ts 5.6 + export const toFileRangesRequestArgs = (file: string, ranges: vscode.Range[]): Proto.FileRangesRequestArgs => ({ + file, + ranges: ranges.map(toFileRange) + }); + export const toFormattingRequestArgs = (file: string, range: vscode.Range): Proto.FormatRequestArgs => ({ file, line: range.start.line + 1, diff --git a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index da651e71044..c44bfc3ac3f 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -90,8 +90,8 @@ export default class TypeScriptServiceClientHost extends Disposable { services, allModeIds)); - this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => { - this.diagnosticsReceived(kind, resource, diagnostics); + this.client.onDiagnosticsReceived(({ kind, resource, diagnostics, spans }) => { + this.diagnosticsReceived(kind, resource, diagnostics, spans); }, null, this._disposables); this.client.onConfigDiagnosticsReceived(diag => this.configFileDiagnosticsReceived(diag), null, this._disposables); @@ -236,14 +236,16 @@ export default class TypeScriptServiceClientHost extends Disposable { private async diagnosticsReceived( kind: DiagnosticKind, resource: vscode.Uri, - diagnostics: Proto.Diagnostic[] + diagnostics: Proto.Diagnostic[], + spans: Proto.TextSpan[] | undefined, ): Promise { const language = await this.findLanguage(resource); if (language) { language.diagnosticsReceived( kind, resource, - this.createMarkerDatas(diagnostics, language.diagnosticSource)); + this.createMarkerDatas(diagnostics, language.diagnosticSource), + spans?.map(span => typeConverters.Range.fromTextSpan(span))); } } diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index 14732491118..931b287df03 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -77,6 +77,7 @@ interface StandardTsServerRequests { 'getMoveToRefactoringFileSuggestions': [Proto.GetMoveToRefactoringFileSuggestionsRequestArgs, Proto.GetMoveToRefactoringFileSuggestions]; 'linkedEditingRange': [Proto.FileLocationRequestArgs, Proto.LinkedEditingRangeResponse]; 'mapCode': [Proto.MapCodeRequestArgs, Proto.MapCodeResponse]; + 'getPasteEdits': [Proto.GetPasteEditsRequestArgs, Proto.GetPasteEditsResponse]; } interface NoResponseTsServerRequests { diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 24742f99219..2435f7725a9 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -37,6 +37,7 @@ export interface TsDiagnostics { readonly kind: DiagnosticKind; readonly resource: vscode.Uri; readonly diagnostics: Proto.Diagnostic[]; + readonly spans?: Proto.TextSpan[]; } interface ToCancelOnResourceChanged { @@ -672,7 +673,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType if (!this._isPromptingAfterCrash) { if (this.pluginManager.plugins.length) { prompt = vscode.window.showWarningMessage( - vscode.l10n.t("The JS/TS language service crashed.\nThis may be caused by a plugin contributed by one of these extensions: {0}.\nPlease try disabling these extensions before filing an issue against VS Code.", pluginExtensionList), reportIssueItem); + vscode.l10n.t("The JS/TS language service crashed.\nThis may be caused by a plugin contributed by one of these extensions: {0}.\nPlease try disabling these extensions before filing an issue against VS Code.", pluginExtensionList)); } else { prompt = vscode.window.showWarningMessage( vscode.l10n.t("The JS/TS language service crashed."), @@ -947,7 +948,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType switch (event.event) { case EventName.syntaxDiag: case EventName.semanticDiag: - case EventName.suggestionDiag: { + case EventName.suggestionDiag: + case EventName.regionSemanticDiag: { // This event also roughly signals that projects have been loaded successfully (since the TS server is synchronous) this.loadingIndicator.reset(); @@ -956,7 +958,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType this._onDiagnosticsReceived.fire({ kind: getDiagnosticsKind(event), resource: this.toResource(diagnosticEvent.body.file), - diagnostics: diagnosticEvent.body.diagnostics + diagnostics: diagnosticEvent.body.diagnostics, + // @ts-expect-error until ts 5.6 + spans: diagnosticEvent.body.spans, }); } break; @@ -1261,6 +1265,7 @@ function getDiagnosticsKind(event: Proto.Event) { case 'syntaxDiag': return DiagnosticKind.Syntax; case 'semanticDiag': return DiagnosticKind.Semantic; case 'suggestionDiag': return DiagnosticKind.Suggestion; + case 'regionSemanticDiag': return DiagnosticKind.RegionSemantic; } throw new Error('Unknown dignostics kind'); } diff --git a/extensions/typescript-language-features/tsconfig.json b/extensions/typescript-language-features/tsconfig.json index 1da85cd17cd..65557839ba6 100644 --- a/extensions/typescript-language-features/tsconfig.json +++ b/extensions/typescript-language-features/tsconfig.json @@ -17,5 +17,6 @@ "../../src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts", "../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts", "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts", + "../../src/vscode-dts/vscode.proposed.documentPaste.d.ts", ] } diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 51a74ad7b00..61828a784fb 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -7,12 +7,14 @@ "enabledApiProposals": [ "activeComment", "authSession", - "defaultChatParticipant", + "chatParticipantPrivate", + "chatProvider", "chatVariableResolver", - "contribViewsRemote", "contribStatusBarItems", + "contribViewsRemote", "createFileSystemWatcher", "customEditorMove", + "defaultChatParticipant", "diffCommand", "documentFiltersExclusive", "documentPaste", @@ -26,6 +28,8 @@ "findTextInFiles", "fsChunks", "interactive", + "languageStatusText", + "lmTools", "mappedEditsProvider", "notebookCellExecutionState", "notebookDeprecated", @@ -34,25 +38,24 @@ "notebookMime", "portsAttributes", "quickPickSortByLabel", - "languageStatusText", "resolvers", "scmActionButton", "scmSelectedProvider", "scmTextDocument", "scmValidation", "taskPresentationGroup", + "telemetry", "terminalDataWriteEvent", "terminalDimensions", "terminalShellIntegration", - "tunnels", "testObserver", "textSearchProvider", "timeline", "tokenInformation", "treeViewActiveItem", "treeViewReveal", - "workspaceTrust", - "telemetry" + "tunnels", + "workspaceTrust" ], "private": true, "activationEvents": [], @@ -62,6 +65,11 @@ }, "icon": "media/icon.png", "contributes": { + "languageModels": [ + { + "vendor": "test-lm-vendor" + } + ], "chatParticipants": [ { "id": "api-test.participant", @@ -104,6 +112,13 @@ "farboo.get": { "type": "string", "default": "get-prop" + }, + "integration-test.http.proxy": { + "type": "string" + }, + "integration-test.http.proxyAuth": { + "type": "string", + "default": "get-prop" } } }, @@ -241,7 +256,10 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "20.x" + "@types/node": "20.x", + "@types/node-forge": "^1.3.11", + "node-forge": "^1.3.1", + "straightforward": "^4.2.2" }, "repository": { "type": "git", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts new file mode 100644 index 00000000000..178119a1197 --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/lm.test.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { assertNoRpc, closeAllEditors, DeferredPromise, disposeAll } from '../utils'; + + +suite('lm', function () { + + let disposables: vscode.Disposable[] = []; + + setup(function () { + disposables = []; + }); + + teardown(async function () { + assertNoRpc(); + await closeAllEditors(); + disposeAll(disposables); + }); + + + test('lm request and stream', async function () { + + let p: vscode.Progress | undefined; + const defer = new DeferredPromise(); + + disposables.push(vscode.lm.registerChatModelProvider('test-lm', { + async provideLanguageModelResponse(_messages, _options, _extensionId, progress, _token) { + p = progress; + return defer.p; + }, + async provideTokenCount(_text, _token) { + return 1; + }, + }, { + name: 'test-lm', + version: '1.0.0', + family: 'test', + vendor: 'test-lm-vendor', + maxInputTokens: 100, + maxOutputTokens: 100, + })); + + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); + assert.strictEqual(models.length, 1); + + const request = await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + + // assert we have a request immediately + assert.ok(request); + assert.ok(p); + assert.strictEqual(defer.isSettled, false); + + let streamDone = false; + let responseText = ''; + + const pp = (async () => { + for await (const chunk of request.text) { + responseText += chunk; + } + streamDone = true; + })(); + + assert.strictEqual(responseText, ''); + assert.strictEqual(streamDone, false); + + p.report({ index: 0, part: 'Hello' }); + defer.complete(); + + await pp; + await new Promise(r => setTimeout(r, 1000)); + + assert.strictEqual(streamDone, true); + assert.strictEqual(responseText, 'Hello'); + }); + + test('lm request fail', async function () { + + disposables.push(vscode.lm.registerChatModelProvider('test-lm', { + async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + throw new Error('BAD'); + }, + async provideTokenCount(_text, _token) { + return 1; + }, + }, { + name: 'test-lm', + version: '1.0.0', + family: 'test', + vendor: 'test-lm-vendor', + maxInputTokens: 100, + maxOutputTokens: 100, + })); + + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); + assert.strictEqual(models.length, 1); + + try { + await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + assert.ok(false, 'EXPECTED error'); + } catch (error) { + assert.ok(error instanceof Error); + } + }); + + test('lm stream fail', async function () { + + const defer = new DeferredPromise(); + + disposables.push(vscode.lm.registerChatModelProvider('test-lm', { + async provideLanguageModelResponse(_messages, _options, _extensionId, _progress, _token) { + return defer.p; + }, + async provideTokenCount(_text, _token) { + return 1; + }, + }, { + name: 'test-lm', + version: '1.0.0', + family: 'test', + vendor: 'test-lm-vendor', + maxInputTokens: 100, + maxOutputTokens: 100, + })); + + const models = await vscode.lm.selectChatModels({ id: 'test-lm' }); + assert.strictEqual(models.length, 1); + + const res = await models[0].sendRequest([vscode.LanguageModelChatMessage.User('Hello')]); + assert.ok(res); + + const result = (async () => { + for await (const _chunk of res.text) { + + } + })(); + + defer.error(new Error('STREAM FAIL')); + + try { + await result; + assert.ok(false, 'EXPECTED error'); + } catch (error) { + assert.ok(error); + // assert.ok(error instanceof Error); // todo@jrieken requires one more insiders + } + }); +}); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts new file mode 100644 index 00000000000..ccda85b442a --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as https from 'https'; +import 'mocha'; +import { assertNoRpc, delay } from '../utils'; +import { pki } from 'node-forge'; +import { AddressInfo } from 'net'; +import { resetCaches } from '@vscode/proxy-agent'; +import * as vscode from 'vscode'; +import { middleware, Straightforward } from 'straightforward'; + +(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('vscode API - network proxy support', () => { + + teardown(async function () { + assertNoRpc(); + }); + + test('custom root certificate', async () => { + const keys = pki.rsa.generateKeyPair(2048); + const cert = pki.createCertificate(); + cert.publicKey = keys.publicKey; + cert.serialNumber = '01'; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); + const attrs = [{ + name: 'commonName', + value: 'localhost-proxy-test' + }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.sign(keys.privateKey); + const certPEM = pki.certificateToPem(cert); + const privateKeyPEM = pki.privateKeyToPem(keys.privateKey); + + let resolvePort: (port: number) => void; + let rejectPort: (err: any) => void; + const port = new Promise((resolve, reject) => { + resolvePort = resolve; + rejectPort = reject; + }); + const server = https.createServer({ + key: privateKeyPEM, + cert: certPEM, + }, (_req, res) => { + res.end(); + }).listen(0, '127.0.0.1', () => { + const address = server.address(); + resolvePort((address as AddressInfo).port); + }).on('error', err => { + rejectPort(err); + }); + + // Using https.globalAgent because it is shared with proxyResolver.ts and mutable. + (https.globalAgent as any).testCertificates = [certPEM]; + resetCaches(); + + try { + const portNumber = await port; + await new Promise((resolve, reject) => { + https.get(`https://127.0.0.1:${portNumber}`, { servername: 'localhost-proxy-test' }, res => { + if (res.statusCode === 200) { + resolve(); + } else { + reject(new Error(`Unexpected status code: ${res.statusCode}`)); + } + }) + .on('error', reject); + }); + } finally { + delete (https.globalAgent as any).testCertificates; + resetCaches(); + server.close(); + } + }); + + test('basic auth', async () => { + const url = 'https://example.com'; // Need to use non-local URL because local URLs are excepted from proxying. + const user = 'testuser'; + const pass = 'testpassword'; + + const sf = new Straightforward(); + let authEnabled = false; + const auth = middleware.auth({ user, pass }); + sf.onConnect.use(async (context, next) => { + if (authEnabled) { + return auth(context, next); + } + next(); + }); + sf.onConnect.use(({ clientSocket }) => { + // Shortcircuit the request. + if (authEnabled) { + clientSocket.end('HTTP/1.1 204\r\n\r\n'); + } else { + clientSocket.end('HTTP/1.1 418\r\n\r\n'); + } + }); + const proxyListen = sf.listen(0); + + try { + await proxyListen; + const proxyPort = (sf.server.address() as AddressInfo).port; + + await vscode.workspace.getConfiguration().update('integration-test.http.proxy', `PROXY 127.0.0.1:${proxyPort}`, vscode.ConfigurationTarget.Global); + await delay(1000); // Wait for the configuration change to propagate. + await new Promise((resolve, reject) => { + https.get(url, res => { + if (res.statusCode === 418) { + resolve(); + } else { + reject(new Error(`Unexpected status code (expected 418): ${res.statusCode}`)); + } + }) + .on('error', reject); + }); + + authEnabled = true; + await new Promise((resolve, reject) => { + https.get(url, res => { + if (res.statusCode === 407) { + resolve(); + } else { + reject(new Error(`Unexpected status code (expected 407): ${res.statusCode}`)); + } + }) + .on('error', reject); + }); + + await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', `${user}:${pass}`, vscode.ConfigurationTarget.Global); + await delay(1000); // Wait for the configuration change to propagate. + await new Promise((resolve, reject) => { + https.get(url, res => { + if (res.statusCode === 204) { + resolve(); + } else { + reject(new Error(`Unexpected status code (expected 204): ${res.statusCode}`)); + } + }) + .on('error', reject); + }); + } finally { + sf.close(); + await vscode.workspace.getConfiguration().update('integration-test.http.proxy', undefined, vscode.ConfigurationTarget.Global); + await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', undefined, vscode.ConfigurationTarget.Global); + } + }); +}); diff --git a/extensions/vscode-api-tests/yarn.lock b/extensions/vscode-api-tests/yarn.lock index 484fa0c5ac5..9999a40aa14 100644 --- a/extensions/vscode-api-tests/yarn.lock +++ b/extensions/vscode-api-tests/yarn.lock @@ -7,6 +7,20 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== +"@types/node-forge@^1.3.11": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.14.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.6.tgz#f3c19ffc98c2220e18de259bb172dd4d892a6075" + integrity sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw== + dependencies: + undici-types "~5.26.4" + "@types/node@20.x": version "20.11.24" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" @@ -14,7 +28,138 @@ dependencies: undici-types "~5.26.4" +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +straightforward@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/straightforward/-/straightforward-4.2.2.tgz#a7d99b313dec5c04b0c637c7b8684dd44dc9167c" + integrity sha512-MxfuNnyTP4RPjadI3DkYIcNIp0DMXeDmAXY4/6QivU8lLIPGUqaS5VsEkaQ2QC+FICzc7QTb/lJPRIhGRKVuMA== + dependencies: + debug "^4.3.4" + yargs "^17.6.2" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.6.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" diff --git a/extensions/vscode-colorize-tests/test/colorize-results/issue-1550_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/issue-1550_yaml.json index dac84162b3c..cc1450808af 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/issue-1550_yaml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/issue-1550_yaml.json @@ -1,7 +1,7 @@ [ { "c": "test1", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -15,7 +15,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -29,7 +29,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -43,7 +43,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": "dsd", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -71,7 +71,7 @@ }, { "c": "test2", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -85,7 +85,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -99,7 +99,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -113,7 +113,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": "abc-def", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -141,7 +141,7 @@ }, { "c": "test-3", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -155,7 +155,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -169,7 +169,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -183,7 +183,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +197,7 @@ }, { "c": "abcdef", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -211,7 +211,7 @@ }, { "c": "test-4", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -225,7 +225,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -239,7 +239,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -253,7 +253,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +267,7 @@ }, { "c": "abc-def", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/issue-4008_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/issue-4008_yaml.json index e1aa82e9ca3..c8c2d57d903 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/issue-4008_yaml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/issue-4008_yaml.json @@ -1,7 +1,7 @@ [ { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -15,7 +15,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -29,7 +29,7 @@ }, { "c": "blue", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -43,7 +43,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": "a=\"brown,not_brown\"", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -85,7 +85,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -99,7 +99,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -113,7 +113,7 @@ }, { "c": "not_blue", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -127,7 +127,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -141,7 +141,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -155,7 +155,7 @@ }, { "c": "foo", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -169,7 +169,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -183,7 +183,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +197,7 @@ }, { "c": "blue", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -211,7 +211,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -225,7 +225,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -239,7 +239,7 @@ }, { "c": "foo=\"}\"", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -253,7 +253,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +267,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -281,7 +281,7 @@ }, { "c": "not_blue", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -295,7 +295,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -309,7 +309,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -323,7 +323,7 @@ }, { "c": "1", - "t": "source.yaml constant.numeric.integer.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml constant.numeric.integer.decimal.yaml", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/issue-6303_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/issue-6303_yaml.json index 2066b677e2c..e5267d62494 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/issue-6303_yaml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/issue-6303_yaml.json @@ -1,7 +1,7 @@ [ { "c": "swagger", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -15,7 +15,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -29,7 +29,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -43,7 +43,7 @@ }, { "c": "'", - "t": "source.yaml string.quoted.single.yaml punctuation.definition.string.begin.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.definition.string.begin.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -57,7 +57,7 @@ }, { "c": "2.0", - "t": "source.yaml string.quoted.single.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -71,7 +71,7 @@ }, { "c": "'", - "t": "source.yaml string.quoted.single.yaml punctuation.definition.string.end.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.definition.string.end.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -85,7 +85,7 @@ }, { "c": "info", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -99,7 +99,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -113,7 +113,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": "description", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -141,7 +141,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -155,7 +155,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -169,7 +169,7 @@ }, { "c": "'", - "t": "source.yaml string.quoted.single.yaml punctuation.definition.string.begin.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.definition.string.begin.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -183,7 +183,7 @@ }, { "c": "The API Management Service API defines an updated and refined version", - "t": "source.yaml string.quoted.single.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -196,8 +196,22 @@ } }, { - "c": " of the concepts currently known as Developer, APP, and API Product in Edge. Of", - "t": "source.yaml string.quoted.single.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -210,8 +224,8 @@ } }, { - "c": " note is the introduction of the API concept, missing previously from Edge", - "t": "source.yaml string.quoted.single.yaml", + "c": "of the concepts currently known as Developer, APP, and API Product in Edge. Of", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -224,8 +238,64 @@ } }, { - "c": " ", - "t": "source.yaml string.quoted.single.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.whitespace.separator.yaml", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.single.yaml: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.single.yaml: #0000FF", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string.quoted.single.yaml: #0F4A85", + "light_modern": "string.quoted.single.yaml: #0000FF" + } + }, + { + "c": "note is the introduction of the API concept, missing previously from Edge", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.single.yaml: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.single.yaml: #0000FF", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string.quoted.single.yaml: #0F4A85", + "light_modern": "string.quoted.single.yaml: #0000FF" + } + }, + { + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -239,7 +309,7 @@ }, { "c": "'", - "t": "source.yaml string.quoted.single.yaml punctuation.definition.string.end.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.quoted.single.yaml punctuation.definition.string.end.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.quoted.single.yaml: #0000FF", @@ -253,7 +323,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +337,7 @@ }, { "c": "title", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -281,7 +351,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -295,7 +365,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -309,7 +379,7 @@ }, { "c": "API Management Service API", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -323,7 +393,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -337,7 +407,7 @@ }, { "c": "version", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -351,7 +421,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -365,7 +435,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -379,7 +449,7 @@ }, { "c": "initial", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-cssvariables_less.json b/extensions/vscode-colorize-tests/test/colorize-results/test-cssvariables_less.json index 856f013541b..3381f6448d0 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-cssvariables_less.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-cssvariables_less.json @@ -686,7 +686,7 @@ } }, { - "c": " 5px", + "c": " ", "t": "source.css.less meta.property-list.less meta.property-value.less meta.function-call.less meta.function-call.less", "r": { "dark_plus": "default: #D4D4D4", @@ -699,6 +699,34 @@ "light_modern": "default: #3B3B3B" } }, + { + "c": "5", + "t": "source.css.less meta.property-list.less meta.property-value.less meta.function-call.less meta.function-call.less constant.numeric.less", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "px", + "t": "source.css.less meta.property-list.less meta.property-value.less meta.function-call.less meta.function-call.less constant.numeric.less keyword.other.unit.less", + "r": { + "dark_plus": "keyword.other.unit: #B5CEA8", + "light_plus": "keyword.other.unit: #098658", + "dark_vs": "keyword.other.unit: #B5CEA8", + "light_vs": "keyword.other.unit: #098658", + "hc_black": "keyword.other.unit: #B5CEA8", + "dark_modern": "keyword.other.unit: #B5CEA8", + "hc_light": "keyword.other.unit: #096D48", + "light_modern": "keyword.other.unit: #098658" + } + }, { "c": ")", "t": "source.css.less meta.property-list.less meta.property-value.less meta.function-call.less meta.function-call.less punctuation.definition.group.end.less", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json index 407cc7c7a1a..9ab753e7556 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json @@ -1,7 +1,7 @@ [ { "c": "#", - "t": "source.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", + "t": "source.yaml meta.stream.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -15,7 +15,7 @@ }, { "c": " sequencer protocols for Laser eye surgery", - "t": "source.yaml comment.line.number-sign.yaml", + "t": "source.yaml meta.stream.yaml comment.line.number-sign.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -29,7 +29,7 @@ }, { "c": "---", - "t": "source.yaml entity.other.document.begin.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml entity.other.document.begin.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -43,7 +43,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": "step", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -85,7 +85,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -99,7 +99,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -113,7 +113,7 @@ }, { "c": "&", - "t": "source.yaml meta.property.yaml keyword.control.property.anchor.yaml punctuation.definition.anchor.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.anchor.yaml punctuation.definition.anchor.yaml", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -127,21 +127,21 @@ }, { "c": "id001", - "t": "source.yaml meta.property.yaml entity.name.type.anchor.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.anchor.yaml variable.other.anchor.yaml", "r": { - "dark_plus": "entity.name.type: #4EC9B0", - "light_plus": "entity.name.type: #267F99", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "entity.name.type: #4EC9B0", - "dark_modern": "entity.name.type: #4EC9B0", - "hc_light": "entity.name.type: #185E73", - "light_modern": "entity.name.type: #267F99" + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" } }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -155,7 +155,7 @@ }, { "c": "#", - "t": "source.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -169,7 +169,7 @@ }, { "c": " defines anchor label &id001", - "t": "source.yaml comment.line.number-sign.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml comment.line.number-sign.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -182,8 +182,22 @@ } }, { - "c": " ", - "t": "source.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +211,7 @@ }, { "c": "instrument", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -211,7 +225,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -225,7 +239,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -239,7 +253,7 @@ }, { "c": "Lasik 2000", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -252,8 +266,22 @@ } }, { - "c": " ", - "t": "source.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +295,7 @@ }, { "c": "pulseEnergy", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -281,7 +309,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -295,7 +323,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -309,7 +337,7 @@ }, { "c": "5.4", - "t": "source.yaml constant.numeric.float.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml constant.numeric.float.yaml", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -322,8 +350,22 @@ } }, { - "c": " ", - "t": "source.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -337,7 +379,7 @@ }, { "c": "spotSize", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -351,7 +393,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -365,7 +407,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -379,7 +421,7 @@ }, { "c": "1mm", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -393,7 +435,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -407,7 +449,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -421,7 +463,7 @@ }, { "c": "step", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -435,7 +477,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -449,7 +491,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -463,7 +505,7 @@ }, { "c": "*", - "t": "source.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -477,12 +519,12 @@ }, { "c": "id001", - "t": "source.yaml variable.other.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml variable.other.alias.yaml", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -491,7 +533,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -505,7 +547,7 @@ }, { "c": "#", - "t": "source.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml comment.line.number-sign.yaml punctuation.definition.comment.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -519,7 +561,7 @@ }, { "c": " refers to the first step (with anchor &id001)", - "t": "source.yaml comment.line.number-sign.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml comment.line.number-sign.yaml", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -533,7 +575,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -547,7 +589,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -561,7 +603,7 @@ }, { "c": "step", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -575,7 +617,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -589,7 +631,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -603,7 +645,7 @@ }, { "c": "*", - "t": "source.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -617,12 +659,12 @@ }, { "c": "id001", - "t": "source.yaml variable.other.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml variable.other.alias.yaml", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -630,8 +672,8 @@ } }, { - "c": " ", - "t": "source.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -644,22 +686,8 @@ } }, { - "c": "spotSize", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", - "r": { - "dark_plus": "entity.name.tag: #569CD6", - "light_plus": "entity.name.tag: #800000", - "dark_vs": "entity.name.tag: #569CD6", - "light_vs": "entity.name.tag: #800000", - "hc_black": "entity.name.tag: #569CD6", - "dark_modern": "entity.name.tag: #569CD6", - "hc_light": "entity.name.tag: #0F4A85", - "light_modern": "entity.name.tag: #800000" - } - }, - { - "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "c": " ", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -671,9 +699,23 @@ "light_modern": "default: #3B3B3B" } }, + { + "c": "spotSize:", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml invalid.illegal.unrecognized.yaml markup.strikethrough", + "r": { + "dark_plus": "invalid: #F44747", + "light_plus": "invalid: #CD3131", + "dark_vs": "invalid: #F44747", + "light_vs": "invalid: #CD3131", + "hc_black": "invalid: #F44747", + "dark_modern": "invalid: #F44747", + "hc_light": "invalid: #B5200D", + "light_modern": "invalid: #CD3131" + } + }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -687,7 +729,7 @@ }, { "c": "2mm", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -701,21 +743,21 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml punctuation.whitespace.separator.yaml", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_plus": "string: #CE9178", + "light_plus": "string.unquoted.plain.out.yaml: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.unquoted.plain.out.yaml: #0000FF", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string.unquoted.plain.out.yaml: #0F4A85", + "light_modern": "string.unquoted.plain.out.yaml: #0000FF" } }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -729,7 +771,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -743,7 +785,7 @@ }, { "c": "step", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -757,7 +799,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -771,7 +813,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -785,7 +827,7 @@ }, { "c": "*", - "t": "source.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -799,12 +841,12 @@ }, { "c": "id002", - "t": "source.yaml variable.other.alias.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml variable.other.alias.yaml", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -813,7 +855,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -827,7 +869,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -841,7 +883,7 @@ }, { "c": "{", - "t": "source.yaml meta.flow-mapping.yaml punctuation.definition.mapping.begin.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml punctuation.definition.mapping.begin.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -855,7 +897,7 @@ }, { "c": "name", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -869,7 +911,7 @@ }, { "c": ":", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -883,7 +925,7 @@ }, { "c": " ", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml meta.flow-pair.value.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -897,7 +939,7 @@ }, { "c": "John Smith", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml meta.flow-pair.value.yaml string.unquoted.plain.in.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml string.unquoted.plain.in.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.in.yaml: #0000FF", @@ -911,7 +953,7 @@ }, { "c": ",", - "t": "source.yaml meta.flow-mapping.yaml punctuation.separator.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml punctuation.separator.mapping.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -925,7 +967,7 @@ }, { "c": " ", - "t": "source.yaml meta.flow-mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -939,7 +981,7 @@ }, { "c": "age", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -953,7 +995,7 @@ }, { "c": ":", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -967,7 +1009,7 @@ }, { "c": " ", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml meta.flow-pair.value.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -981,7 +1023,7 @@ }, { "c": "33", - "t": "source.yaml meta.flow-mapping.yaml meta.flow-pair.yaml meta.flow-pair.value.yaml constant.numeric.integer.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml meta.flow.map.implicit.yaml meta.flow.pair.value.yaml string.unquoted.plain.in.yaml constant.numeric.integer.decimal.yaml", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -995,7 +1037,7 @@ }, { "c": "}", - "t": "source.yaml meta.flow-mapping.yaml punctuation.definition.mapping.end.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.flow.mapping.yaml punctuation.definition.mapping.end.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1009,7 +1051,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1023,7 +1065,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1037,7 +1079,7 @@ }, { "c": "name", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -1051,7 +1093,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1065,7 +1107,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1079,7 +1121,7 @@ }, { "c": "Mary Smith", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -1093,7 +1135,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1107,7 +1149,7 @@ }, { "c": "age", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -1121,7 +1163,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1135,7 +1177,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1149,7 +1191,7 @@ }, { "c": "27", - "t": "source.yaml constant.numeric.integer.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml string.unquoted.plain.out.yaml constant.numeric.integer.decimal.yaml", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1163,7 +1205,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1176,8 +1218,22 @@ } }, { - "c": "men", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "c": "m", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml invalid.illegal.expected-indentation.yaml", + "r": { + "dark_plus": "invalid: #F44747", + "light_plus": "invalid: #CD3131", + "dark_vs": "invalid: #F44747", + "light_vs": "invalid: #CD3131", + "hc_black": "invalid: #F44747", + "dark_modern": "invalid: #F44747", + "hc_light": "invalid: #B5200D", + "light_modern": "invalid: #CD3131" + } + }, + { + "c": "en", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -1191,7 +1247,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1205,7 +1261,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1219,7 +1275,7 @@ }, { "c": "[", - "t": "source.yaml meta.flow-sequence.yaml punctuation.definition.sequence.begin.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml punctuation.definition.sequence.begin.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1233,7 +1289,7 @@ }, { "c": "John Smith", - "t": "source.yaml meta.flow-sequence.yaml string.unquoted.plain.in.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml string.unquoted.plain.in.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.in.yaml: #0000FF", @@ -1247,7 +1303,7 @@ }, { "c": ",", - "t": "source.yaml meta.flow-sequence.yaml punctuation.separator.sequence.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml punctuation.separator.sequence.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1261,7 +1317,7 @@ }, { "c": " ", - "t": "source.yaml meta.flow-sequence.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1275,7 +1331,7 @@ }, { "c": "Bill Jones", - "t": "source.yaml meta.flow-sequence.yaml string.unquoted.plain.in.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml string.unquoted.plain.in.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.in.yaml: #0000FF", @@ -1289,7 +1345,7 @@ }, { "c": "]", - "t": "source.yaml meta.flow-sequence.yaml punctuation.definition.sequence.end.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.flow.sequence.yaml punctuation.definition.sequence.end.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1302,8 +1358,36 @@ } }, { - "c": "women", - "t": "source.yaml string.unquoted.plain.out.yaml entity.name.tag.yaml", + "c": "w", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml invalid.illegal.expected-indentation.yaml", + "r": { + "dark_plus": "invalid: #F44747", + "light_plus": "invalid: #CD3131", + "dark_vs": "invalid: #F44747", + "light_vs": "invalid: #CD3131", + "hc_black": "invalid: #F44747", + "dark_modern": "invalid: #F44747", + "hc_light": "invalid: #B5200D", + "light_modern": "invalid: #CD3131" + } + }, + { + "c": "o", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml invalid.illegal.expected-indentation.yaml", + "r": { + "dark_plus": "invalid: #F44747", + "light_plus": "invalid: #CD3131", + "dark_vs": "invalid: #F44747", + "light_vs": "invalid: #CD3131", + "hc_black": "invalid: #F44747", + "dark_modern": "invalid: #F44747", + "hc_light": "invalid: #B5200D", + "light_modern": "invalid: #CD3131" + } + }, + { + "c": "men", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", "r": { "dark_plus": "entity.name.tag: #569CD6", "light_plus": "entity.name.tag: #800000", @@ -1317,7 +1401,7 @@ }, { "c": ":", - "t": "source.yaml punctuation.separator.key-value.mapping.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml punctuation.separator.map.value.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1331,7 +1415,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1345,7 +1429,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1359,7 +1443,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1373,7 +1457,7 @@ }, { "c": "Mary Smith", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", @@ -1387,7 +1471,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml punctuation.whitespace.indentation.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1401,7 +1485,7 @@ }, { "c": "-", - "t": "source.yaml punctuation.definition.block.sequence.item.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml punctuation.definition.block.sequence.item.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1415,7 +1499,7 @@ }, { "c": " ", - "t": "source.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml punctuation.whitespace.separator.yaml", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1429,7 +1513,7 @@ }, { "c": "Susan Williams", - "t": "source.yaml string.unquoted.plain.out.yaml", + "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml meta.block.sequence.yaml string.unquoted.plain.out.yaml", "r": { "dark_plus": "string: #CE9178", "light_plus": "string.unquoted.plain.out.yaml: #0000FF", diff --git a/extensions/yaml/build/update-grammar.js b/extensions/yaml/build/update-grammar.js new file mode 100644 index 00000000000..8684bc3e5d0 --- /dev/null +++ b/extensions/yaml/build/update-grammar.js @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +var updateGrammar = require('vscode-grammar-updater'); + +async function updateGrammars() { + await updateGrammar.update('RedCMD/YAML-Syntax-Highlighter', 'syntaxes/yaml-1.0.tmLanguage.json', './syntaxes/yaml-1.0.tmLanguage.json', undefined, 'main'); + await updateGrammar.update('RedCMD/YAML-Syntax-Highlighter', 'syntaxes/yaml-1.1.tmLanguage.json', './syntaxes/yaml-1.1.tmLanguage.json', undefined, 'main'); + await updateGrammar.update('RedCMD/YAML-Syntax-Highlighter', 'syntaxes/yaml-1.2.tmLanguage.json', './syntaxes/yaml-1.2.tmLanguage.json', undefined, 'main'); + await updateGrammar.update('RedCMD/YAML-Syntax-Highlighter', 'syntaxes/yaml-1.3.tmLanguage.json', './syntaxes/yaml-1.3.tmLanguage.json', undefined, 'main'); + await updateGrammar.update('RedCMD/YAML-Syntax-Highlighter', 'syntaxes/yaml.tmLanguage.json', './syntaxes/yaml.tmLanguage.json', undefined, 'main'); +} + +updateGrammars(); diff --git a/extensions/yaml/cgmanifest.json b/extensions/yaml/cgmanifest.json index e6c3ca158b5..43dc4ca637e 100644 --- a/extensions/yaml/cgmanifest.json +++ b/extensions/yaml/cgmanifest.json @@ -4,33 +4,24 @@ "component": { "type": "git", "git": { - "name": "textmate/yaml.tmbundle", - "repositoryUrl": "https://github.com/textmate/yaml.tmbundle", - "commitHash": "e54ceae3b719506dba7e481a77cea4a8b576ae46" + "name": "RedCMD/YAML-Syntax-Highlighter", + "repositoryUrl": "https://github.com/RedCMD/YAML-Syntax-Highlighter", + "commitHash": "287c71aeb0773759497822b5e5ce4bdc4d5ef2aa" } }, "licenseDetail": [ - "Copyright (c) 2015 FichteFoll ", + "MIT License", "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", + "Copyright 2024 RedCMD", "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Softwareâ€), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED “AS ISâ€, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." ], - "license": "TextMate Bundle License", - "version": "0.0.0" + "license": "MIT", + "version": "1.0.0" } ], "version": 1 diff --git a/extensions/yaml/package.json b/extensions/yaml/package.json index 5223f71c52b..d19c507bdfe 100644 --- a/extensions/yaml/package.json +++ b/extensions/yaml/package.json @@ -9,7 +9,7 @@ "vscode": "*" }, "scripts": { - "update-grammar": "node ../node_modules/vscode-grammar-updater/bin textmate/yaml.tmbundle Syntaxes/YAML.tmLanguage ./syntaxes/yaml.tmLanguage.json" + "update-grammar": "node ./build/update-grammar.js" }, "categories": ["Programming Languages"], "contributes": { @@ -56,6 +56,22 @@ "scopeName": "source.yaml", "path": "./syntaxes/yaml.tmLanguage.json" }, + { + "scopeName": "source.yaml.1.3", + "path": "./syntaxes/yaml-1.3.tmLanguage.json" + }, + { + "scopeName": "source.yaml.1.2", + "path": "./syntaxes/yaml-1.2.tmLanguage.json" + }, + { + "scopeName": "source.yaml.1.1", + "path": "./syntaxes/yaml-1.1.tmLanguage.json" + }, + { + "scopeName": "source.yaml.1.0", + "path": "./syntaxes/yaml-1.0.tmLanguage.json" + }, { "language": "yaml", "scopeName": "source.yaml", diff --git a/extensions/yaml/syntaxes/yaml-1.0.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.0.tmLanguage.json new file mode 100644 index 00000000000..9f2a401ca5e --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.0.tmLanguage.json @@ -0,0 +1,1621 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/RedCMD/YAML-Syntax-Highlighter/blob/master/syntaxes/yaml-1.0.tmLanguage.json", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/RedCMD/YAML-Syntax-Highlighter/commit/287c71aeb0773759497822b5e5ce4bdc4d5ef2aa", + "name": "YAML 1.0", + "scopeName": "source.yaml.1.0", + "comment": "https://yaml.org/spec/1.0/", + "patterns": [ + { + "include": "#stream" + } + ], + "repository": { + "stream": { + "patterns": [ + { + "comment": "allows me to just use `\\G` instead of the performance heavy `(^|\\G)`", + "begin": "^(?!\\G)", + "while": "^", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G", + "while": "\\G", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-YAML": { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "(?=%YAML:1\\.0(?=[\\x{85 2028 2029}\r\n\t ]))", + "end": "\\G(?=%(?!YAML:1\\.0))", + "name": "meta.1.0.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "\\G(%)(YAML)(:)(1\\.0)", + "while": "\\G(?!---[\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.yaml.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "constant.numeric.yaml-version.yaml" + } + }, + "name": "meta.directives.yaml", + "patterns": [ + { + "include": "#directive-invalid" + }, + { + "include": "#directives" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G(?=---[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?!%)", + "patterns": [ + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#presentation-detail" + } + ] + }, + "directives": { + "comment": "https://yaml.org/spec/1.2.2/#68-directives", + "patterns": [ + { + "include": "source.yaml.1.3#directive-YAML" + }, + { + "include": "source.yaml.1.2#directive-YAML" + }, + { + "include": "source.yaml.1.1#directive-YAML" + }, + { + "include": "source.yaml.1.0#directive-YAML" + }, + { + "begin": "(?=%)", + "while": "\\G(?!%|---[\\x{85 2028 2029}\r\n\t ])", + "name": "meta.directives.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-reserved-directive", + "begin": "(%)([^: \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.other.yaml" + } + }, + "patterns": [ + { + "match": "\\G(:)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-name.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "match": "\\G\\.{3}(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-invalid": { + "patterns": [ + { + "match": "\\G\\.{3}(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "begin": "\\G(%)(YAML)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "invalid.illegal.keyword.other.directive.yaml.yaml" + } + }, + "name": "meta.directive.yaml", + "patterns": [ + { + "match": "\\G([\t ]++|:)([0-9]++\\.[0-9]++)?+", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "constant.numeric.yaml-version.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "document": { + "comment": "https://yaml.org/spec/1.2.2/#91-documents", + "patterns": [ + { + "begin": "---(?=[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?!(?>\\.{3}|---)[\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "0": { + "name": "entity.other.document.begin.yaml" + } + }, + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + { + "begin": "(?=\\.{3}[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?=[\t \\x{FEFF}]*+(?>#|$))", + "patterns": [ + { + "begin": "\\G\\.{3}", + "end": "$", + "beginCaptures": { + "0": { + "name": "entity.other.document.end.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G(?!%|[\t \\x{FEFF}]*+(?>#|$))", + "while": "\\G(?!(?>\\.{3}|---)[\\x{85 2028 2029}\r\n\t ])", + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + } + ] + }, + "block-node": { + "patterns": [ + { + "include": "#block-sequence" + }, + { + "include": "#block-mapping" + }, + { + "include": "#block-scalar" + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "while": "\\G", + "patterns": [ + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "begin": "(?=\\[|{)", + "while": "\\G", + "patterns": [ + { + "include": "#block-mapping" + }, + { + "begin": "(?!\\G)(?![\r\n\t ])", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "#block-plain-out" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-mapping": { + "//": "The check for plain keys is expensive", + "begin": "(?=((?<=[-?:]) )?+)(?[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Plain)(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))(?>[^:#]++|:(?![\\x{85 2028 2029}\r\n\t ])|(?(\\1\\2)((?>[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t ?:\\-#!&*\"'\\[\\]{}0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}])?+|( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + }, + "5": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "4": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "5": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.mapping.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", + "begin": "\\G'", + "end": "'(?!')", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "match": "''", + "name": "constant.character.escape.single-quote.yaml" + } + ] + }, + { + "include": "#block-key-plain" + }, + { + "include": "#block-map-value" + }, + { + "include": "#block-map-explicit" + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#rule-l+block-sequence", + "begin": "(?=((?<=[-?:]) )?+)(?(\\1\\2)(?!-[\\x{85 2028 2029}\r\n\t ])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\]}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.block.sequence.item.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.block.sequence.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-map-explicit": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-explicit-key", + "begin": "(?=((?<=[-?:]) )?+)\\G( *+)(\\?)(?=[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029 FEFF}]])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.map.key.yaml" + }, + "4": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.map.explicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-key-plain-out" + }, + { + "include": "#block-map-value" + }, + { + "include": "#block-node" + } + ] + }, + "block-map-value": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", + "begin": "(:)(?=[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029 FEFF}]]|-[^\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "1": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.map.value.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-key-plain": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (BLOCK-KEY)", + "begin": "\\G(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", + "end": "(?=[\t ]*+:[\\x{85 2028 2029}\r\n\t ]|(?>[\t ]++|\\G)#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G([\t ]++)(.)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "invalid.illegal.multiline-key.yaml" + } + } + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "block-scalar": { + "comment": "https://yaml.org/spec/1.2.2/#81-block-scalar-styles", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator", + "begin": "([\t ]*+)(?>(\\|)|(>))(?[+-])?+((0)|(1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)|(9))(?()|([+-]))?+", + "while": "\\G(?>(?>(?!\\6)|(?!\\7) |(?!\\8) {2}|(?!\\9) {3}|(?!\\10) {4}|(?!\\11) {5}|(?!\\12) {6}|(?!\\13) {7}|(?!\\14) {8}|(?!\\15) {9})| *+($|[^#]))", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + }, + "5": { + "name": "constant.numeric.indentation-indicator.yaml" + }, + "16": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "whileCaptures": { + "0": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "1": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "begin": "$", + "while": "\\G", + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + { + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-b-block-header", + "//": "Soooooooo many edge cases", + "begin": "([\t ]*+)(?>(\\|)|(>))([+-]?+)", + "while": "\\G", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-literal-content", + "begin": "$", + "while": "\\G", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "//": "Find the highest indented line", + "begin": "\\G( ++)$", + "while": "\\G(?>(\\1)$|(?!\\1)( *+)($|.))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-b-nb-literal-next", + "//": [ + "Funky wrapper function", + "The `end` pattern clears the parent `\\G` anchor", + "Affectively forcing this rule to only match at most once", + "https://github.com/microsoft/vscode-textmate/issues/114" + ], + "begin": "\\G(?!$)(?=( *+))", + "end": "\\G(?!\\1)(?=[\t ]*+#)", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "begin": "\\G( *+)", + "while": "\\G(?>(\\1)|( *+)($|[^\t#]|[\t ]++[^#]))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-chomped-empty", + "begin": "(?!\\G)(?=[\t ]*+#)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + { + "comment": "Header Comment", + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + } + ] + }, + "block-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-OUT)", + "begin": "(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", + "while": "\\G", + "patterns": [ + { + "begin": "\\G", + "end": "(?=(?>[\t ]++|\\G)#)", + "name": "string.unquoted.plain.out.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": ":(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-node": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-seq-entry (FLOW-IN)", + "patterns": [ + { + "begin": "(?=\\[|{)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "include": "#flow-plain-in" + }, + { + "include": "#presentation-detail" + } + ] + }, + "flow-mapping": { + "comment": "https://yaml.org/spec/1.2.2/#742-flow-mappings", + "begin": "{", + "end": "}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.mapping.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.mapping.end.yaml" + } + }, + "name": "meta.flow.mapping.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-map-entries", + "begin": "(?<={)\\G(?=[\\x{85 2028 2029}\r\n\t ,#])|,", + "end": "(?=[^\\x{85 2028 2029}\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.mapping.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-map-key-mapping" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#741-flow-sequences", + "begin": "\\[", + "end": "]", + "beginCaptures": { + "0": { + "name": "punctuation.definition.sequence.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.sequence.end.yaml" + } + }, + "name": "meta.flow.sequence.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-seq-entries", + "begin": "(?<=\\[)\\G(?=[\\x{85 2028 2029}\r\n\t ,#])|,", + "end": "(?=[^\\x{85 2028 2029}\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.sequence.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-map-key-sequence" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-map-key-mapping": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-map-key-mapping" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])))", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#flow-key-plain-in" + }, + { + "match": ":(?=\\[|{)", + "name": "invalid.illegal.separator.map.yaml" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=\"|')", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-map-key-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-map-key-mapping" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?<=[\t ,\\[{]|^)(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))(?>[^:#,\\[\\]{}]++|:(?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])|(?\"(?>[^\\\\\"]++|\\\\.)*+\"|'(?>[^']++|'')*+')[\t ]*+:)", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-map-value-yaml": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": ":(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-map-value-json": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": "(?<=(?>[\"'\\]}]|^)[\t ]*+):", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-IN)", + "begin": "(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "end": "(?=(?>[\t ]++|\\G)#|[\t ]*+[,\\[\\]{}])", + "name": "string.unquoted.plain.in.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": ":(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "flow-key-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (FLOW-OUT)", + "begin": "(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", + "end": "(?=[\t ]*+:[\\x{85 2028 2029}\r\n\t ]|[\t ]++#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "flow-key-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-implicit-yaml-key (FLOW-KEY)", + "begin": "\\G(?![\\x{85 2028 2029}\r\n\t #])", + "end": "(?=[\t ]*+(?>:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]|[,\\[\\]{}])|[\t ]++#)", + "name": "meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "include": "#non-printable" + } + ] + }, + "key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "key-single": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", + "begin": "\\G'", + "end": "'(?!')", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "match": "''", + "name": "constant.character.escape.single-quote.yaml" + } + ] + }, + "double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "string.quoted.double.yaml", + "patterns": [ + { + "match": "(?x[^\"]{2,0}|u[^\"]{4,0}|U[^\"]{8,0}|.)", + "name": "invalid.illegal.constant.character.escape.yaml" + } + ] + }, + "tag-implicit-plain-in": { + "comment": "https://yaml.org/type/index.html", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE|y|Y|yes|Yes|YES|n|N|no|No|NO|on|On|ON|off|Off|OFF)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[-+]?+(0|[1-9][0-9_]*+)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G[-+]?+0b[0-1_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.binary.yaml" + }, + { + "match": "\\G[-+]?0[0-7_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G[-+]?+0x[0-9a-fA-F_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[-+]?+[1-9][0-9_]*+(?>:[0-5]?[0-9])++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+(?>[0-9][0-9_]*+)?+\\.[0-9.]*+(?>[eE][-+][0-9]+)?+(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.decimal.yaml" + }, + { + "match": "\\G[-+]?+[0-9][0-9_]*+(?>:[0-5]?[0-9])++\\.[0-9_]*+(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.nan.yaml" + }, + { + "comment": "https://www.w3.org/TR/NOTE-datetime does not allow spaces, however https://yaml.org/type/timestamp.html does, but the provided regex doesn't match the TZD space in many of the YAML examples", + "match": "\\G(?>[0-9]{4}-[0-9]{2,1}-[0-9]{2,1}(?>T|t|[\t ]++)[0-9]{2,1}:[0-9]{2}:[0-9]{2}(?>\\.[0-9]*+)?+[\t ]*+(?>Z|[-+][0-9]{2,1}(?>:[0-9]{2})?+)?+|[0-9]{4}-[0-9]{2}-[0-9]{2})(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.timestamp.yaml" + }, + { + "match": "\\G<<(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.merge.yaml" + }, + { + "match": "\\G=(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.value.yaml" + }, + { + "match": "\\G(?>!|&|\\*)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.yaml.yaml" + } + ] + }, + "tag-implicit-plain-out": { + "comment": "https://yaml.org/type/index.html", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE|yes|Yes|YES|y|Y|no|No|NO|n|N|on|On|ON|off|Off|OFF)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[-+]?+(0|[1-9][0-9_]*+)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G[-+]?+0b[0-1_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.binary.yaml" + }, + { + "match": "\\G[-+]?0[0-7_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G[-+]?+0x[0-9a-fA-F_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[-+]?+[1-9][0-9_]*+(?>:[0-5]?[0-9])++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+(?>[0-9][0-9_]*+)?+\\.[0-9.]*+(?>[eE][-+][0-9]+)?+(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.decimal.yaml" + }, + { + "match": "\\G[-+]?+[0-9][0-9_]*+(?>:[0-5]?[0-9])++\\.[0-9_]*+(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.nan.yaml" + }, + { + "comment": "https://www.w3.org/TR/NOTE-datetime does not allow spaces, however https://yaml.org/type/timestamp.html does, but the provided regex doesn't match the TZD space in many of the YAML examples", + "match": "\\G(?>[0-9]{4}-[0-9]{2,1}-[0-9]{2,1}(?>T|t|[\t ]++)[0-9]{2,1}:[0-9]{2}:[0-9]{2}(?>\\.[0-9]*+)?+[\t ]*+(?>Z|[-+][0-9]{2,1}(?>:[0-9]{2})?+)?+|[0-9]{4}-[0-9]{2}-[0-9]{2})(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.timestamp.yaml" + }, + { + "match": "\\G<<(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.merge.yaml" + }, + { + "match": "\\G=(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.value.yaml" + }, + { + "match": "\\G(?>!|&|\\*)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.yaml.yaml" + } + ] + }, + "tag-property": { + "comment": "https://yaml.org/spec/1.0/#c-ns-tag-property", + "//": [ + "!^", + "!!private_ns-tag-char+", + "!global_core_ns-tag-char+_no-:/!", + "!global_vocabulary_az09-_/ns-tag-char", + "!global_domain_ns-tag-char+.ns-tag-char+,1234(-12(-12)?)?/ns-tag-char*" + ], + "begin": "(?=!)", + "end": "(?=[\\x{2028 2029}\r\n\t ])", + "name": "storage.type.tag.yaml", + "patterns": [ + { + "match": "\\G!(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "punctuation.definition.tag.non-specific.yaml" + }, + { + "comment": "https://yaml.org/spec/1.0/#c-ns-private-tag", + "match": "\\G!!", + "name": "punctuation.definition.tag.private.yaml" + }, + { + "comment": "https://yaml.org/spec/1.0/#ns-ns-global-tag", + "match": "\\G!", + "name": "punctuation.definition.tag.global.yaml" + }, + { + "comment": "https://yaml.org/spec/1.0/#c-prefix", + "match": "\\^", + "name": "punctuation.definition.tag.prefix.yaml" + }, + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\\x{85 2028 2029}\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "#double-escape" + }, + { + "include": "#non-printable" + } + ] + }, + "anchor-property": { + "match": "(&)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)|(&)", + "captures": { + "0": { + "name": "keyword.control.flow.anchor.yaml" + }, + "1": { + "name": "punctuation.definition.anchor.yaml" + }, + "2": { + "name": "variable.other.anchor.yaml" + }, + "3": { + "name": "invalid.illegal.flow.anchor.yaml" + } + } + }, + "alias": { + "begin": "(\\*)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)|(\\*)", + "end": "(?=:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]|[,\\[\\]{}])", + "captures": { + "0": { + "name": "keyword.control.flow.alias.yaml" + }, + "1": { + "name": "punctuation.definition.alias.yaml" + }, + "2": { + "name": "variable.other.alias.yaml" + }, + "3": { + "name": "invalid.illegal.flow.alias.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + "presentation-detail": { + "patterns": [ + { + "match": "[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "#non-printable" + }, + { + "include": "#comment" + }, + { + "include": "#unknown" + } + ] + }, + "non-printable": { + "//": { + "85": "Â…", + "2028": "
", + "2029": "
", + "10000": "ð€€", + "A0": " ", + "D7FF": "퟿", + "E000": "", + "FFFD": "�", + "FFFF": "ï¿¿", + "10FFFF": "ô¿¿" + }, + "match": "[^\t\n\r -~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]++", + "name": "invalid.illegal.non-printable.yaml" + }, + "comment": { + "comment": "Comments must be separated from other tokens by white space characters. `space`, `tab`, `newline` or `carriage-return`. `#(.*)` causes performance issues", + "begin": "(?<=^|[\\x{85 2028 2029} ])#", + "end": "[\\x{85 2028 2029}\r\n]", + "captures": { + "0": { + "name": "punctuation.definition.comment.yaml" + } + }, + "name": "comment.line.number-sign.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + "unknown": { + "match": ".[[^\\x{85}#\"':,\\[\\]{}]&&!-~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", + "name": "invalid.illegal.unrecognized.yaml markup.strikethrough" + } + } +} \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml-1.1.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.1.tmLanguage.json new file mode 100644 index 00000000000..bda3a191ce8 --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.1.tmLanguage.json @@ -0,0 +1,1792 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/RedCMD/YAML-Syntax-Highlighter/blob/master/syntaxes/yaml-1.1.tmLanguage.json", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/RedCMD/YAML-Syntax-Highlighter/commit/287c71aeb0773759497822b5e5ce4bdc4d5ef2aa", + "name": "YAML 1.1", + "scopeName": "source.yaml.1.1", + "comment": "https://yaml.org/spec/1.1/", + "patterns": [ + { + "include": "#stream" + } + ], + "repository": { + "stream": { + "patterns": [ + { + "comment": "allows me to just use `\\G` instead of the performance heavy `(^|\\G)`", + "begin": "^(?!\\G)", + "while": "^", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G", + "while": "\\G", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-YAML": { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "(?=%YAML[ \t]+1\\.1(?=[\\x{85 2028 2029}\r\n\t ]))", + "end": "\\G(?=%(?!YAML[ \t]+1\\.1))", + "name": "meta.1.1.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "\\G(%)(YAML)([ \t]+)(1\\.1)", + "while": "\\G(?!---[\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.yaml.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "constant.numeric.yaml-version.yaml" + } + }, + "name": "meta.directives.yaml", + "patterns": [ + { + "include": "#directive-invalid" + }, + { + "include": "#directives" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G(?=---[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?!%)", + "patterns": [ + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#presentation-detail" + } + ] + }, + "directives": { + "comment": "https://yaml.org/spec/1.2.2/#68-directives", + "patterns": [ + { + "include": "source.yaml.1.3#directive-YAML" + }, + { + "include": "source.yaml.1.2#directive-YAML" + }, + { + "include": "source.yaml.1.1#directive-YAML" + }, + { + "include": "source.yaml.1.0#directive-YAML" + }, + { + "begin": "(?=%)", + "while": "\\G(?!%|---[\\x{85 2028 2029}\r\n\t ])", + "name": "meta.directives.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#682-tag-directives", + "begin": "\\G(%)(TAG)(?>([\t ]++)((!)(?>[0-9A-Za-z-]*+(!))?+))?+", + "end": "$", + "applyEndPatternLast": true, + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.tag.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "storage.type.tag-handle.yaml" + }, + "5": { + "name": "punctuation.definition.tag.begin.yaml" + }, + "6": { + "name": "punctuation.definition.tag.end.yaml" + }, + "comment": "https://yaml.org/spec/1.2.2/#rule-c-tag-handle" + }, + "patterns": [ + { + "comment": "technically the beginning should only validate against a valid uri scheme [A-Za-z][A-Za-z0-9.+-]*", + "begin": "\\G[\t ]++(?!#)", + "end": "(?=[\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "contentName": "support.type.tag-prefix.yaml", + "patterns": [ + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\\x{85 2028 2029}\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "\\G[,\\[\\]{}]", + "name": "invalid.illegal.character.uri.yaml" + }, + { + "include": "#non-printable" + }, + { + "match": "[^\\x{85 2028 2029}\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-reserved-directive", + "begin": "(%)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.other.yaml" + } + }, + "patterns": [ + { + "match": "\\G([\t ]++)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-name.yaml" + } + } + }, + { + "match": "([\t ]++)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-parameter.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "match": "\\G\\.{3}(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-invalid": { + "patterns": [ + { + "match": "\\G\\.{3}(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "begin": "\\G(%)(YAML)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "invalid.illegal.keyword.other.directive.yaml.yaml" + } + }, + "name": "meta.directive.yaml", + "patterns": [ + { + "match": "\\G([\t ]++|:)([0-9]++\\.[0-9]++)?+", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "constant.numeric.yaml-version.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "document": { + "comment": "https://yaml.org/spec/1.2.2/#91-documents", + "patterns": [ + { + "begin": "---(?=[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?!(?>\\.{3}|---)[\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "0": { + "name": "entity.other.document.begin.yaml" + } + }, + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + { + "begin": "(?=\\.{3}[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?=[\t \\x{FEFF}]*+(?>#|$))", + "patterns": [ + { + "begin": "\\G\\.{3}", + "end": "$", + "beginCaptures": { + "0": { + "name": "entity.other.document.end.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#byte-order-mark" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G(?!%|[\t \\x{FEFF}]*+(?>#|$))", + "while": "\\G(?!(?>\\.{3}|---)[\\x{85 2028 2029}\r\n\t ])", + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + } + ] + }, + "block-node": { + "patterns": [ + { + "include": "#block-sequence" + }, + { + "include": "#block-mapping" + }, + { + "include": "#block-scalar" + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "while": "\\G", + "patterns": [ + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "begin": "(?=\\[|{)", + "while": "\\G", + "patterns": [ + { + "include": "#block-mapping" + }, + { + "begin": "(?!\\G)(?![\r\n\t ])", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "#block-plain-out" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-mapping": { + "//": "The check for plain keys is expensive", + "begin": "(?=((?<=[-?:]) )?+)(?[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Plain)(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))(?>[^:#]++|:(?![\\x{85 2028 2029}\r\n\t ])|(?(\\1\\2)((?>[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t ?:\\-#!&*\"'\\[\\]{}0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}])?+|( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + }, + "5": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "4": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "5": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.mapping.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", + "begin": "\\G'", + "end": "'(?!')", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "match": "''", + "name": "constant.character.escape.single-quote.yaml" + } + ] + }, + { + "include": "#block-key-plain" + }, + { + "include": "#block-map-value" + }, + { + "include": "#block-map-explicit" + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#rule-l+block-sequence", + "begin": "(?=((?<=[-?:]) )?+)(?(\\1\\2)(?!-[\\x{85 2028 2029}\r\n\t ])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\]}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.block.sequence.item.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.block.sequence.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-map-explicit": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-explicit-key", + "begin": "(?=((?<=[-?:]) )?+)\\G( *+)(\\?)(?=[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029 FEFF}]])((?>\t[\t ]*+)?+[^\\x{85 2028 2029}\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\\x{85 2028 2029}\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.map.key.yaml" + }, + "4": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.map.explicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-key-plain-out" + }, + { + "include": "#block-map-value" + }, + { + "include": "#block-node" + } + ] + }, + "block-map-value": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", + "begin": "(:)(?=[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{2028 2029 FEFF}]]|-[^\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "1": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.map.value.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-key-plain": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (BLOCK-KEY)", + "begin": "\\G(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", + "end": "(?=[\t ]*+:[\\x{85 2028 2029}\r\n\t ]|(?>[\t ]++|\\G)#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G([\t ]++)(.)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "invalid.illegal.multiline-key.yaml" + } + } + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "block-scalar": { + "comment": "https://yaml.org/spec/1.2.2/#81-block-scalar-styles", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator", + "begin": "([\t ]*+)(?>(\\|)|(>))(?[+-])?+((1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)|(9))(?()|([+-]))?+", + "while": "\\G(?>(?>(?!\\6) |(?!\\7) {2}|(?!\\8) {3}|(?!\\9) {4}|(?!\\10) {5}|(?!\\11) {6}|(?!\\12) {7}|(?!\\13) {8}|(?!\\14) {9})| *+($|[^#]))", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + }, + "5": { + "name": "constant.numeric.indentation-indicator.yaml" + }, + "15": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "whileCaptures": { + "0": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "1": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "begin": "$", + "while": "\\G", + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + { + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-b-block-header", + "//": "Soooooooo many edge cases", + "begin": "([\t ]*+)(?>(\\|)|(>))([+-]?+)", + "while": "\\G", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-literal-content", + "begin": "$", + "while": "\\G", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "//": "Find the highest indented line", + "begin": "\\G( ++)$", + "while": "\\G(?>(\\1)$|(?!\\1)( *+)($|.))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-b-nb-literal-next", + "//": [ + "Funky wrapper function", + "The `end` pattern clears the parent `\\G` anchor", + "Affectively forcing this rule to only match at most once", + "https://github.com/microsoft/vscode-textmate/issues/114" + ], + "begin": "\\G(?!$)(?=( *+))", + "end": "\\G(?!\\1)(?=[\t ]*+#)", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "begin": "\\G( *+)", + "while": "\\G(?>(\\1)|( *+)($|[^\t#]|[\t ]++[^#]))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-chomped-empty", + "begin": "(?!\\G)(?=[\t ]*+#)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + { + "comment": "Header Comment", + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + } + ] + }, + "block-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-OUT)", + "begin": "(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", + "while": "\\G", + "patterns": [ + { + "begin": "\\G", + "end": "(?=(?>[\t ]++|\\G)#)", + "name": "string.unquoted.plain.out.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": ":(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-node": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-seq-entry (FLOW-IN)", + "patterns": [ + { + "begin": "(?=\\[|{)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "include": "#flow-plain-in" + }, + { + "include": "#presentation-detail" + } + ] + }, + "flow-mapping": { + "comment": "https://yaml.org/spec/1.2.2/#742-flow-mappings", + "begin": "{", + "end": "}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.mapping.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.mapping.end.yaml" + } + }, + "name": "meta.flow.mapping.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-map-entries", + "begin": "(?<={)\\G(?=[\\x{85 2028 2029}\r\n\t ,#])|,", + "end": "(?=[^\\x{85 2028 2029}\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.mapping.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-map-key-mapping" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#741-flow-sequences", + "begin": "\\[", + "end": "]", + "beginCaptures": { + "0": { + "name": "punctuation.definition.sequence.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.sequence.end.yaml" + } + }, + "name": "meta.flow.sequence.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-seq-entries", + "begin": "(?<=\\[)\\G(?=[\\x{85 2028 2029}\r\n\t ,#])|,", + "end": "(?=[^\\x{85 2028 2029}\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.sequence.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-map-key-sequence" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-map-key-mapping": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-map-key-mapping" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])))", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#flow-key-plain-in" + }, + { + "match": ":(?=\\[|{)", + "name": "invalid.illegal.separator.map.yaml" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=\"|')", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-map-key-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-map-key-mapping" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?<=[\t ,\\[{]|^)(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))(?>[^:#,\\[\\]{}]++|:(?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])|(?\"(?>[^\\\\\"]++|\\\\.)*+\"|'(?>[^']++|'')*+')[\t ]*+:)", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-map-value-yaml": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": ":(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-map-value-json": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": "(?<=(?>[\"'\\]}]|^)[\t ]*+):", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-IN)", + "begin": "(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "end": "(?=(?>[\t ]++|\\G)#|[\t ]*+[,\\[\\]{}])", + "name": "string.unquoted.plain.in.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": ":(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "flow-key-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (FLOW-OUT)", + "begin": "(?=[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ]))", + "end": "(?=[\t ]*+:[\\x{85 2028 2029}\r\n\t ]|[\t ]++#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "flow-key-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-implicit-yaml-key (FLOW-KEY)", + "begin": "\\G(?![\\x{85 2028 2029}\r\n\t #])", + "end": "(?=[\t ]*+(?>:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]|[,\\[\\]{}])|[\t ]++#)", + "name": "meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "include": "#non-printable" + } + ] + }, + "key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "key-single": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", + "begin": "\\G'", + "end": "'(?!')", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "match": "''", + "name": "constant.character.escape.single-quote.yaml" + } + ] + }, + "double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "string.quoted.double.yaml", + "patterns": [ + { + "match": "(?x[^\"]{2,0}|u[^\"]{4,0}|U[^\"]{8,0}|.)", + "name": "invalid.illegal.constant.character.escape.yaml" + } + ] + }, + "tag-implicit-plain-in": { + "comment": "https://yaml.org/type/index.html", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE|y|Y|yes|Yes|YES|n|N|no|No|NO|on|On|ON|off|Off|OFF)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[-+]?+(0|[1-9][0-9_]*+)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G[-+]?+0b[0-1_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.binary.yaml" + }, + { + "match": "\\G[-+]?0[0-7_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G[-+]?+0x[0-9a-fA-F_]++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[-+]?+[1-9][0-9_]*+(?>:[0-5]?[0-9])++(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+(?>[0-9][0-9_]*+)?+\\.[0-9.]*+(?>[eE][-+][0-9]+)?+(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.decimal.yaml" + }, + { + "match": "\\G[-+]?+[0-9][0-9_]*+(?>:[0-5]?[0-9])++\\.[0-9_]*+(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.nan.yaml" + }, + { + "comment": "https://www.w3.org/TR/NOTE-datetime does not allow spaces, however https://yaml.org/type/timestamp.html does, but the provided regex doesn't match the TZD space in many of the YAML examples", + "match": "\\G(?>[0-9]{4}-[0-9]{2,1}-[0-9]{2,1}(?>T|t|[\t ]++)[0-9]{2,1}:[0-9]{2}:[0-9]{2}(?>\\.[0-9]*+)?+[\t ]*+(?>Z|[-+][0-9]{2,1}(?>:[0-9]{2})?+)?+|[0-9]{4}-[0-9]{2}-[0-9]{2})(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.timestamp.yaml" + }, + { + "match": "\\G<<(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.merge.yaml" + }, + { + "match": "\\G=(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.value.yaml" + }, + { + "match": "\\G(?>!|&|\\*)(?=[\t ]++#|[\t ]*+(?>[\\x{85 2028 2029}\r\n,\\]}]|:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]))", + "name": "constant.language.yaml.yaml" + } + ] + }, + "tag-implicit-plain-out": { + "comment": "https://yaml.org/type/index.html", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE|yes|Yes|YES|y|Y|no|No|NO|n|N|on|On|ON|off|Off|OFF)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[-+]?+(0|[1-9][0-9_]*+)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G[-+]?+0b[0-1_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.binary.yaml" + }, + { + "match": "\\G[-+]?0[0-7_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G[-+]?+0x[0-9a-fA-F_]++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[-+]?+[1-9][0-9_]*+(?>:[0-5]?[0-9])++(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.integer.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+(?>[0-9][0-9_]*+)?+\\.[0-9.]*+(?>[eE][-+][0-9]+)?+(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.decimal.yaml" + }, + { + "match": "\\G[-+]?+[0-9][0-9_]*+(?>:[0-5]?[0-9])++\\.[0-9_]*+(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.Sexagesimal.yaml" + }, + { + "match": "\\G[-+]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.float.nan.yaml" + }, + { + "comment": "https://www.w3.org/TR/NOTE-datetime does not allow spaces, however https://yaml.org/type/timestamp.html does, but the provided regex doesn't match the TZD space in many of the YAML examples", + "match": "\\G(?>[0-9]{4}-[0-9]{2,1}-[0-9]{2,1}(?>T|t|[\t ]++)[0-9]{2,1}:[0-9]{2}:[0-9]{2}(?>\\.[0-9]*+)?+[\t ]*+(?>Z|[-+][0-9]{2,1}(?>:[0-9]{2})?+)?+|[0-9]{4}-[0-9]{2}-[0-9]{2})(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.numeric.timestamp.yaml" + }, + { + "match": "\\G<<(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.merge.yaml" + }, + { + "match": "\\G=(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.value.yaml" + }, + { + "match": "\\G(?>!|&|\\*)(?=[\t ]++#|[\t ]*+(?>$|:[\\x{85 2028 2029}\r\n\t ]))", + "name": "constant.language.yaml.yaml" + } + ] + }, + "tag-property": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-tag-property", + "//": [ + "!", + "!!", + "!<>", + "!...", + "!!...", + "!<...>", + "!...!..." + ], + "patterns": [ + { + "match": "!(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "storage.type.tag.non-specific.yaml punctuation.definition.tag.non-specific.yaml" + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-verbatim-tag", + "begin": "!<", + "end": ">", + "beginCaptures": { + "0": { + "name": "punctuation.definition.tag.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag.end.yaml" + } + }, + "name": "storage.type.tag.verbatim.yaml", + "patterns": [ + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\\x{85 2028 2029}\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "#non-printable" + }, + { + "match": "[^\\x{85 2028 2029}\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]%>]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-shorthand-tag", + "begin": "(?=!)", + "end": "(?=[\\x{85 2028 2029}\r\n\t ,\\[\\]{}])", + "name": "storage.type.tag.shorthand.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", + "match": "\\G!!", + "name": "punctuation.definition.tag.secondary.yaml" + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", + "match": "\\G(!)[0-9A-Za-z-]++(!)", + "captures": { + "1": { + "name": "punctuation.definition.tag.named.yaml" + }, + "2": { + "name": "punctuation.definition.tag.named.yaml" + } + } + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-primary-tag-handle", + "match": "\\G!", + "name": "punctuation.definition.tag.primary.yaml" + }, + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\\x{85 2028 2029}\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "#non-printable" + }, + { + "match": "[^\\x{85 2028 2029}\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.~*'()\\[\\]%]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + } + ] + }, + "anchor-property": { + "match": "(&)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)|(&)", + "captures": { + "0": { + "name": "keyword.control.flow.anchor.yaml" + }, + "1": { + "name": "punctuation.definition.anchor.yaml" + }, + "2": { + "name": "variable.other.anchor.yaml" + }, + "3": { + "name": "invalid.illegal.flow.anchor.yaml" + } + } + }, + "alias": { + "begin": "(\\*)([^ \\p{Cntrl}\\p{Surrogate}\\x{2028 2029 FFFE FFFF}]++)|(\\*)", + "end": "(?=:[\\x{85 2028 2029}\r\n\t ,\\[\\]{}]|[,\\[\\]{}])", + "captures": { + "0": { + "name": "keyword.control.flow.alias.yaml" + }, + "1": { + "name": "punctuation.definition.alias.yaml" + }, + "2": { + "name": "variable.other.alias.yaml" + }, + "3": { + "name": "invalid.illegal.flow.alias.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + "byte-order-mark": { + "comment": "", + "begin": "\\G", + "while": "\\G(?=[\\x{FEFF 85 2028 2029}\r\n\t ])", + "patterns": [ + { + "begin": "(?=#)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G\\x{FEFF}", + "while": "\\G", + "beginCaptures": { + "0": { + "name": "byte-order-mark.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#presentation-detail" + } + ] + }, + "presentation-detail": { + "patterns": [ + { + "match": "[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "comment": "https://yaml.org/spec/1.1/#id871136", + "match": "[\\x{85 2028 2029}\r\n]++", + "name": "punctuation.separator.line-break.yaml" + }, + { + "include": "#non-printable" + }, + { + "include": "#comment" + }, + { + "include": "#unknown" + } + ] + }, + "non-printable": { + "//": { + "85": "Â…", + "2028": "
", + "2029": "
", + "10000": "ð€€", + "A0": " ", + "D7FF": "퟿", + "E000": "", + "FFFD": "�", + "FEFF": "", + "FFFF": "ï¿¿", + "10FFFF": "ô¿¿" + }, + "match": "[^\t\n\r -~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]++", + "name": "invalid.illegal.non-printable.yaml" + }, + "comment": { + "comment": "Comments must be separated from other tokens by white space characters. `space`, `newline` or `carriage-return`. `#(.*)` causes performance issues", + "begin": "(?<=^|[\\x{FEFF 85 2028 2029} ])#", + "end": "[\\x{85 2028 2029}\r\n]", + "captures": { + "0": { + "name": "punctuation.definition.comment.yaml" + } + }, + "name": "comment.line.number-sign.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + "unknown": { + "match": ".[[^\\x{85}#\"':,\\[\\]{}]&&!-~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", + "name": "invalid.illegal.unrecognized.yaml markup.strikethrough" + } + } +} \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml-1.2.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.2.tmLanguage.json new file mode 100644 index 00000000000..b2a921a5dd1 --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.2.tmLanguage.json @@ -0,0 +1,1714 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/RedCMD/YAML-Syntax-Highlighter/blob/master/syntaxes/yaml-1.2.tmLanguage.json", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/RedCMD/YAML-Syntax-Highlighter/commit/287c71aeb0773759497822b5e5ce4bdc4d5ef2aa", + "name": "YAML 1.2", + "scopeName": "source.yaml.1.2", + "comment": "https://yaml.org/spec/1.2.2", + "patterns": [ + { + "include": "#stream" + } + ], + "repository": { + "stream": { + "patterns": [ + { + "comment": "allows me to just use `\\G` instead of the performance heavy `(^|\\G)`", + "begin": "^(?!\\G)", + "while": "^", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G", + "while": "\\G", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-YAML": { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "(?=%YAML[\t ]+1\\.2(?=[\r\n\t ]))", + "end": "\\G(?=(?>\\.{3}|---)[\r\n\t ])", + "name": "meta.1.2.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "\\G(%)(YAML)([\t ]+)(1\\.2)", + "end": "\\G(?=---[\r\n\t ])", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.yaml.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "constant.numeric.yaml-version.yaml" + } + }, + "name": "meta.directives.yaml", + "patterns": [ + { + "include": "#directive-invalid" + }, + { + "include": "#directives" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#document" + } + ] + }, + "directives": { + "comment": "https://yaml.org/spec/1.2.2/#68-directives", + "patterns": [ + { + "include": "source.yaml.1.3#directive-YAML" + }, + { + "include": "source.yaml.1.2#directive-YAML" + }, + { + "include": "source.yaml.1.1#directive-YAML" + }, + { + "include": "source.yaml.1.0#directive-YAML" + }, + { + "begin": "(?=%)", + "while": "\\G(?!%|---[\r\n\t ])", + "name": "meta.directives.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#682-tag-directives", + "begin": "\\G(%)(TAG)(?>([\t ]++)((!)(?>[0-9A-Za-z-]*+(!))?+))?+", + "end": "$", + "applyEndPatternLast": true, + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.tag.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "storage.type.tag-handle.yaml" + }, + "5": { + "name": "punctuation.definition.tag.begin.yaml" + }, + "6": { + "name": "punctuation.definition.tag.end.yaml" + }, + "comment": "https://yaml.org/spec/1.2.2/#rule-c-tag-handle" + }, + "patterns": [ + { + "comment": "technically the beginning should only validate against a valid uri scheme [A-Za-z][A-Za-z0-9.+-]*", + "begin": "\\G[\t ]++(?!#)", + "end": "(?=[\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "contentName": "support.type.tag-prefix.yaml", + "patterns": [ + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "\\G[,\\[\\]{}]", + "name": "invalid.illegal.character.uri.yaml" + }, + { + "include": "#non-printable" + }, + { + "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-reserved-directive", + "begin": "(%)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.other.yaml" + } + }, + "patterns": [ + { + "match": "\\G([\t ]++)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-name.yaml" + } + } + }, + { + "match": "([\t ]++)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-parameter.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "match": "\\G\\.{3}(?=[\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-invalid": { + "patterns": [ + { + "match": "\\G\\.{3}(?=[\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "begin": "\\G(%)(YAML)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "invalid.illegal.keyword.other.directive.yaml.yaml" + } + }, + "name": "meta.directive.yaml", + "patterns": [ + { + "match": "\\G([\t ]++|:)([0-9]++\\.[0-9]++)?+", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "constant.numeric.yaml-version.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "document": { + "comment": "https://yaml.org/spec/1.2.2/#91-documents", + "patterns": [ + { + "begin": "---(?=[\r\n\t ])", + "while": "\\G(?!(?>\\.{3}|---)[\r\n\t ])", + "beginCaptures": { + "0": { + "name": "entity.other.document.begin.yaml" + } + }, + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + { + "begin": "(?=\\.{3}[\r\n\t ])", + "while": "\\G(?=[\t \\x{FEFF}]*+(?>#|$))", + "patterns": [ + { + "begin": "\\G\\.{3}", + "end": "$", + "beginCaptures": { + "0": { + "name": "entity.other.document.end.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#byte-order-mark" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G(?!%|[\t \\x{FEFF}]*+(?>#|$))", + "while": "\\G(?!(?>\\.{3}|---)[\r\n\t ])", + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + } + ] + }, + "block-node": { + "patterns": [ + { + "include": "#block-sequence" + }, + { + "include": "#block-mapping" + }, + { + "include": "#block-scalar" + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "while": "\\G", + "patterns": [ + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "begin": "(?=\\[|{)", + "while": "\\G", + "patterns": [ + { + "include": "#block-mapping" + }, + { + "begin": "(?!\\G)(?![\r\n\t ])", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "#block-plain-out" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-mapping": { + "//": "The check for plain keys is expensive", + "begin": "(?=((?<=[-?:]) )?+)(?((?>[!&*][^\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Plain)(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))(?>[^:#]++|:(?![\r\n\t ])|(?(\\1\\2)((?>[!&*][^\r\n\t ]*+[\t ]++)*+)((?>\t[\t ]*+)?+[^\r\n\t ?:\\-#!&*\"'\\[\\]{}0-9A-Za-z$()+./;<=\\\\^_~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}])?+|( *+)([\t ]*+[^\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + }, + "5": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "4": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "5": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.mapping.yaml", + "patterns": [ + { + "include": "#block-map-key-double" + }, + { + "include": "#block-map-key-single" + }, + { + "include": "#block-map-key-plain" + }, + { + "include": "#block-map-key-explicit" + }, + { + "include": "#block-map-value" + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#rule-l+block-sequence", + "begin": "(?=((?<=[-?:]) )?+)(?(\\1\\2)(?!-[\r\n\t ])((?>\t[\t ]*+)?+[^\r\n\t #\\]}])?+|(?!\\1\\2)( *+)([\t ]*+[^\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.block.sequence.item.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.block.sequence.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-map-key-explicit": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-explicit-key", + "begin": "(?=((?<=[-?:]) )?+)\\G( *+)(\\?)(?=[\r\n\t ])", + "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{FEFF}]])((?>\t[\t ]*+)?+[^\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.map.key.yaml" + }, + "4": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.map.explicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-key-plain-out" + }, + { + "include": "#block-map-value" + }, + { + "include": "#block-node" + } + ] + }, + "block-map-key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "block-map-key-single": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", + "begin": "\\G'", + "end": "'(?!')", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "match": "''", + "name": "constant.character.escape.single-quote.yaml" + } + ] + }, + "block-map-key-plain": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (BLOCK-KEY)", + "begin": "\\G(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", + "end": "(?=[\t ]*+:[\r\n\t ]|(?>[\t ]++|\\G)#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G([\t ]++)(.)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "invalid.illegal.multiline-key.yaml" + } + } + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "block-map-value": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", + "begin": ":(?=[\r\n\t ])", + "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{FEFF}]]|-[^\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.map.value.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-scalar": { + "comment": "https://yaml.org/spec/1.2.2/#81-block-scalar-styles", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator", + "begin": "([\t ]*+)(?>(\\|)|(>))(?[+-])?+((1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)|(9))(?()|([+-]))?+", + "while": "\\G(?>(?>(?!\\6) |(?!\\7) {2}|(?!\\8) {3}|(?!\\9) {4}|(?!\\10) {5}|(?!\\11) {6}|(?!\\12) {7}|(?!\\13) {8}|(?!\\14) {9})| *+($|[^#]))", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + }, + "5": { + "name": "constant.numeric.indentation-indicator.yaml" + }, + "15": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "whileCaptures": { + "0": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "1": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "begin": "$", + "while": "\\G", + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + { + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-b-block-header", + "//": "Soooooooo many edge cases", + "begin": "([\t ]*+)(?>(\\|)|(>))([+-]?+)", + "while": "\\G", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-literal-content", + "begin": "$", + "while": "\\G", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "//": "Find the highest indented line", + "begin": "\\G( ++)$", + "while": "\\G(?>(\\1)$|(?!\\1)( *+)($|.))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-b-nb-literal-next", + "//": [ + "Funky wrapper function", + "The `end` pattern clears the parent `\\G` anchor", + "Affectively forcing this rule to only match at most once", + "https://github.com/microsoft/vscode-textmate/issues/114" + ], + "begin": "\\G(?!$)(?=( *+))", + "end": "\\G(?!\\1)(?=[\t ]*+#)", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "begin": "\\G( *+)", + "while": "\\G(?>(\\1)|( *+)($|[^\t#]|[\t ]++[^#]))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-chomped-empty", + "begin": "(?!\\G)(?=[\t ]*+#)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + { + "comment": "Header Comment", + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + } + ] + }, + "block-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-OUT)", + "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", + "while": "\\G", + "patterns": [ + { + "begin": "\\G", + "end": "(?=(?>[\t ]++|\\G)#)", + "name": "string.unquoted.plain.out.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": ":(?=[\r\n\t ])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-node": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-seq-entry (FLOW-IN)", + "patterns": [ + { + "begin": "(?=\\[|{)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "include": "#flow-plain-in" + }, + { + "include": "#presentation-detail" + } + ] + }, + "flow-mapping": { + "comment": "https://yaml.org/spec/1.2.2/#742-flow-mappings", + "begin": "{", + "end": "}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.mapping.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.mapping.end.yaml" + } + }, + "name": "meta.flow.mapping.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-map-entries", + "begin": "(?<={)\\G(?=[\r\n\t ,#])|,", + "end": "(?=[^\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.mapping.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#741-flow-sequences", + "begin": "\\[", + "end": "]", + "beginCaptures": { + "0": { + "name": "punctuation.definition.sequence.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.sequence.end.yaml" + } + }, + "name": "meta.flow.sequence.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-seq-entries", + "begin": "(?<=\\[)\\G(?=[\r\n\t ,#])|,", + "end": "(?=[^\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.sequence.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-sequence-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-mapping-map-key": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}])))", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#flow-key-plain-in" + }, + { + "match": ":(?=\\[|{)", + "name": "invalid.illegal.separator.map.yaml" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=\"|')", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-sequence-map-key": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?<=[\t ,\\[{]|^)(?=(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}]))(?>[^:#,\\[\\]{}]++|:(?![\r\n\t ,\\[\\]{}])|(?\"(?>[^\\\\\"]++|\\\\.)*+\"|'(?>[^']++|'')*+')[\t ]*+:)", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-map-value-yaml": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": ":(?=[\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-map-value-json": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": "(?<=(?>[\"'\\]}]|^)[\t ]*+):", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-IN)", + "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}]))", + "end": "(?=(?>[\t ]++|\\G)#|[\t ]*+[,\\[\\]{}])", + "name": "string.unquoted.plain.in.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": ":(?=[\r\n\t ,\\[\\]{}])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "flow-key-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (FLOW-OUT)", + "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", + "end": "(?=[\t ]*+:[\r\n\t ]|[\t ]++#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "flow-key-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-implicit-yaml-key (FLOW-KEY)", + "begin": "\\G(?![\r\n\t #])", + "end": "(?=[\t ]*+(?>:[\r\n\t ,\\[\\]{}]|[,\\[\\]{}])|[\t ]++#)", + "name": "meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "key-single": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", + "begin": "\\G'", + "end": "'(?!')", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "match": "''", + "name": "constant.character.escape.single-quote.yaml" + } + ] + }, + "double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "string.quoted.double.yaml", + "patterns": [ + { + "match": "(?x[^\"]{2,0}|u[^\"]{4,0}|U[^\"]{8,0}|.)", + "name": "invalid.illegal.constant.character.escape.yaml" + } + ] + }, + "tag-implicit-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#103-core-schema", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[+-]?+[0-9]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G0o[0-7]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G0x[0-9a-fA-F]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[+-]?+(?>\\.[0-9]++|[0-9]++(?>\\.[0-9]*+)?+)(?>[eE][+-]?+[0-9]++)?+(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.yaml" + }, + { + "match": "\\G[+-]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.nan.yaml" + } + ] + }, + "tag-implicit-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#103-core-schema", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[+-]?+[0-9]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G0o[0-7]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G0x[0-9a-fA-F]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[+-]?+(?>\\.[0-9]++|[0-9]++(?>\\.[0-9]*+)?+)(?>[eE][+-]?+[0-9]++)?+(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.float.yaml" + }, + { + "match": "\\G[+-]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.float.nan.yaml" + } + ] + }, + "tag-property": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-tag-property", + "//": [ + "!", + "!!", + "!<>", + "!...", + "!!...", + "!<...>", + "!...!..." + ], + "patterns": [ + { + "match": "!(?=[\r\n\t ])", + "name": "storage.type.tag.non-specific.yaml punctuation.definition.tag.non-specific.yaml" + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-verbatim-tag", + "begin": "!<", + "end": ">", + "beginCaptures": { + "0": { + "name": "punctuation.definition.tag.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag.end.yaml" + } + }, + "name": "storage.type.tag.verbatim.yaml", + "patterns": [ + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "#non-printable" + }, + { + "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]%>]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-shorthand-tag", + "begin": "(?=!)", + "end": "(?=[\r\n\t ,\\[\\]{}])", + "name": "storage.type.tag.shorthand.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", + "match": "\\G!!", + "name": "punctuation.definition.tag.secondary.yaml" + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", + "match": "\\G(!)[0-9A-Za-z-]++(!)", + "captures": { + "1": { + "name": "punctuation.definition.tag.named.yaml" + }, + "2": { + "name": "punctuation.definition.tag.named.yaml" + } + } + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-primary-tag-handle", + "match": "\\G!", + "name": "punctuation.definition.tag.primary.yaml" + }, + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "#non-printable" + }, + { + "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$_.~*'()%]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + } + ] + }, + "anchor-property": { + "match": "(&)([\\x{85}[^ ,\\[\\]{}\\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)|(&)", + "captures": { + "0": { + "name": "keyword.control.flow.anchor.yaml" + }, + "1": { + "name": "punctuation.definition.anchor.yaml" + }, + "2": { + "name": "variable.other.anchor.yaml" + }, + "3": { + "name": "invalid.illegal.flow.anchor.yaml" + } + } + }, + "alias": { + "begin": "(\\*)([\\x{85}[^ ,\\[\\]{}\\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)|(\\*)", + "end": "(?=:[\r\n\t ,\\[\\]{}]|[,\\[\\]{}])", + "captures": { + "0": { + "name": "keyword.control.flow.alias.yaml" + }, + "1": { + "name": "punctuation.definition.alias.yaml" + }, + "2": { + "name": "variable.other.alias.yaml" + }, + "3": { + "name": "invalid.illegal.flow.alias.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + "byte-order-mark": { + "comment": "", + "match": "\\G\\x{FEFF}++", + "name": "byte-order-mark.yaml" + }, + "presentation-detail": { + "patterns": [ + { + "match": "[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "#non-printable" + }, + { + "include": "#comment" + }, + { + "include": "#unknown" + } + ] + }, + "non-printable": { + "//": { + "85": "Â…", + "10000": "ð€€", + "A0": " ", + "D7FF": "퟿", + "E000": "", + "FFFD": "�", + "FEFF": "", + "FFFF": "ï¿¿", + "10FFFF": "ô¿¿" + }, + "//match": "[\\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}&&[^\t\n\r\\x{85}]]++", + "match": "[^\t\n\r -~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]++", + "name": "invalid.illegal.non-printable.yaml" + }, + "comment": { + "comment": "Comments must be separated from other tokens by white space characters. `space`, `tab`, `newline` or `carriage-return`. `#(.*)` causes performance issues", + "begin": "(?<=[\\x{FEFF}\t ]|^)#", + "end": "\r|\n", + "captures": { + "0": { + "name": "punctuation.definition.comment.yaml" + } + }, + "name": "comment.line.number-sign.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + "unknown": { + "match": ".[[^\"':,\\[\\]{}]&&!-~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", + "name": "invalid.illegal.unrecognized.yaml markup.strikethrough" + } + } +} \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml-1.3.tmLanguage.json b/extensions/yaml/syntaxes/yaml-1.3.tmLanguage.json new file mode 100644 index 00000000000..56444fd9fa2 --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.3.tmLanguage.json @@ -0,0 +1,1714 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/RedCMD/YAML-Syntax-Highlighter/blob/master/syntaxes/yaml-1.3.tmLanguage.json", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/RedCMD/YAML-Syntax-Highlighter/commit/287c71aeb0773759497822b5e5ce4bdc4d5ef2aa", + "name": "YAML 1.3", + "scopeName": "source.yaml.1.3", + "comment": "https://spec.yaml.io/main/spec/1.3.0/", + "patterns": [ + { + "include": "#stream" + } + ], + "repository": { + "stream": { + "patterns": [ + { + "comment": "allows me to just use `\\G` instead of the performance heavy `(^|\\G)`", + "begin": "^(?!\\G)", + "while": "^", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G", + "while": "\\G", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-YAML": { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "(?=%YAML[\t ]+1\\.3(?=[\r\n\t ]))", + "end": "\\G(?=(?>\\.{3}|---)[\r\n\t ])", + "name": "meta.1.3.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#681-yaml-directives", + "begin": "\\G(%)(YAML)([\t ]+)(1\\.3)", + "end": "\\G(?=---[\r\n\t ])", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.yaml.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "constant.numeric.yaml-version.yaml" + } + }, + "name": "meta.directives.yaml", + "patterns": [ + { + "include": "#directive-invalid" + }, + { + "include": "#directives" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#document" + } + ] + }, + "directives": { + "comment": "https://yaml.org/spec/1.2.2/#68-directives", + "patterns": [ + { + "include": "source.yaml.1.3#directive-YAML" + }, + { + "include": "source.yaml.1.2#directive-YAML" + }, + { + "include": "source.yaml.1.1#directive-YAML" + }, + { + "include": "source.yaml.1.0#directive-YAML" + }, + { + "begin": "(?=%)", + "while": "\\G(?!%|---[\r\n\t ])", + "name": "meta.directives.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#682-tag-directives", + "begin": "\\G(%)(TAG)(?>([\t ]++)((!)(?>[0-9A-Za-z-]*+(!))?+))?+", + "end": "$", + "applyEndPatternLast": true, + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.tag.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "name": "storage.type.tag-handle.yaml" + }, + "5": { + "name": "punctuation.definition.tag.begin.yaml" + }, + "6": { + "name": "punctuation.definition.tag.end.yaml" + }, + "comment": "https://yaml.org/spec/1.2.2/#rule-c-tag-handle" + }, + "patterns": [ + { + "comment": "technically the beginning should only validate against a valid uri scheme [A-Za-z][A-Za-z0-9.+-]*", + "begin": "\\G[\t ]++(?!#)", + "end": "(?=[\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "contentName": "support.type.tag-prefix.yaml", + "patterns": [ + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "\\G[,\\[\\]{}]", + "name": "invalid.illegal.character.uri.yaml" + }, + { + "include": "#non-printable" + }, + { + "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-reserved-directive", + "begin": "(%)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "keyword.other.directive.other.yaml" + } + }, + "patterns": [ + { + "match": "\\G([\t ]++)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-name.yaml" + } + } + }, + { + "match": "([\t ]++)([\\x{85}[^ \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "string.unquoted.directive-parameter.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "match": "\\G\\.{3}(?=[\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "directive-invalid": { + "patterns": [ + { + "match": "\\G\\.{3}(?=[\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "begin": "\\G(%)(YAML)", + "end": "$", + "beginCaptures": { + "1": { + "name": "punctuation.definition.directive.begin.yaml" + }, + "2": { + "name": "invalid.illegal.keyword.other.directive.yaml.yaml" + } + }, + "name": "meta.directive.yaml", + "patterns": [ + { + "match": "\\G([\t ]++|:)([0-9]++\\.[0-9]++)?+", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "constant.numeric.yaml-version.yaml" + } + } + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "document": { + "comment": "https://yaml.org/spec/1.2.2/#91-documents", + "patterns": [ + { + "begin": "---(?=[\r\n\t ])", + "while": "\\G(?!(?>\\.{3}|---)[\r\n\t ])", + "beginCaptures": { + "0": { + "name": "entity.other.document.begin.yaml" + } + }, + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + { + "begin": "(?=\\.{3}[\r\n\t ])", + "while": "\\G(?=[\t \\x{FEFF}]*+(?>#|$))", + "patterns": [ + { + "begin": "\\G\\.{3}", + "end": "$", + "beginCaptures": { + "0": { + "name": "entity.other.document.end.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#byte-order-mark" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "\\G(?!%|[\t \\x{FEFF}]*+(?>#|$))", + "while": "\\G(?!(?>\\.{3}|---)[\r\n\t ])", + "name": "meta.document.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + } + ] + }, + "block-node": { + "patterns": [ + { + "include": "#block-sequence" + }, + { + "include": "#block-mapping" + }, + { + "include": "#block-scalar" + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "while": "\\G", + "patterns": [ + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "begin": "(?=\\[|{)", + "while": "\\G", + "patterns": [ + { + "include": "#block-mapping" + }, + { + "begin": "(?!\\G)(?![\r\n\t ])", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "#block-plain-out" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-mapping": { + "//": "The check for plain keys is expensive", + "begin": "(?=((?<=[-?:]) )?+)(?((?>[!&*][^\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Plain)(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))(?>[^:#]++|:(?![\r\n\t ])|(?(\\1\\2)((?>[!&*][^\r\n\t ]*+[\t ]++)*+)((?>\t[\t ]*+)?+[^\r\n\t ?:\\-#!&*\"'\\[\\]{}0-9A-Za-z$()+./;<=\\\\^_~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}])?+|( *+)([\t ]*+[^\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.separator.yaml" + }, + "4": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + }, + "5": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "comment": "May cause lag on long lines starting with a tag, anchor or alias", + "patterns": [ + { + "include": "#tag-property" + }, + { + "include": "#anchor-property" + }, + { + "include": "#alias" + }, + { + "include": "#presentation-detail" + } + ] + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "4": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "5": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.mapping.yaml", + "patterns": [ + { + "include": "#block-map-key-double" + }, + { + "include": "#block-map-key-single" + }, + { + "include": "#block-map-key-plain" + }, + { + "include": "#block-map-key-explicit" + }, + { + "include": "#block-map-value" + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" + } + ] + }, + "block-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#rule-l+block-sequence", + "begin": "(?=((?<=[-?:]) )?+)(?(\\1\\2)(?!-[\r\n\t ])((?>\t[\t ]*+)?+[^\r\n\t #\\]}])?+|(?!\\1\\2)( *+)([\t ]*+[^\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.block.sequence.item.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.block.sequence.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-map-key-explicit": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-explicit-key", + "begin": "(?=((?<=[-?:]) )?+)\\G( *+)(\\?)(?=[\r\n\t ])", + "while": "\\G(?>(\\1\\2)(?![?:0-9A-Za-z$()+./;<=\\\\^_~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{FEFF}]])((?>\t[\t ]*+)?+[^\r\n\t #\\-\\[\\]{}])?+|(?!\\1\\2)( *+)([\t ]*+[^\r\n#])?+)", + "beginCaptures": { + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "punctuation.definition.map.key.yaml" + }, + "4": { + "name": "punctuation.whitespace.separator.yaml" + } + }, + "whileCaptures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "invalid.illegal.expected-indentation.yaml" + }, + "3": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "4": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.map.explicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-key-plain-out" + }, + { + "include": "#block-map-value" + }, + { + "include": "#block-node" + } + ] + }, + "block-map-key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (BLOCK-KEY)", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "block-map-key-single": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", + "begin": "\\G'", + "end": "'(?!')", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "match": "''", + "name": "constant.character.escape.single-quote.yaml" + } + ] + }, + "block-map-key-plain": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (BLOCK-KEY)", + "begin": "\\G(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", + "end": "(?=[\t ]*+:[\r\n\t ]|(?>[\t ]++|\\G)#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G([\t ]++)(.)", + "captures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "invalid.illegal.multiline-key.yaml" + } + } + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "block-map-value": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-l-block-map-implicit-value", + "begin": ":(?=[\r\n\t ])", + "while": "\\G(?![?:!\"'0-9A-Za-z$()+./;<=\\\\^_~\\[{\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}&&[^\\x{FEFF}]]|-[^\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.map.value.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "block-scalar": { + "comment": "https://yaml.org/spec/1.2.2/#81-block-scalar-styles", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator", + "begin": "([\t ]*+)(?>(\\|)|(>))(?[+-])?+((1)|(2)|(3)|(4)|(5)|(6)|(7)|(8)|(9))(?()|([+-]))?+", + "while": "\\G(?>(?>(?!\\6) |(?!\\7) {2}|(?!\\8) {3}|(?!\\9) {4}|(?!\\10) {5}|(?!\\11) {6}|(?!\\12) {7}|(?!\\13) {8}|(?!\\14) {9})| *+($|[^#]))", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + }, + "5": { + "name": "constant.numeric.indentation-indicator.yaml" + }, + "15": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "whileCaptures": { + "0": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "1": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "begin": "$", + "while": "\\G", + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + { + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-b-block-header", + "//": "Soooooooo many edge cases", + "begin": "([\t ]*+)(?>(\\|)|(>))([+-]?+)", + "while": "\\G", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.separator.yaml" + }, + "2": { + "name": "keyword.control.flow.block-scalar.literal.yaml" + }, + "3": { + "name": "keyword.control.flow.block-scalar.folded.yaml" + }, + "4": { + "name": "storage.modifier.chomping-indicator.yaml" + } + }, + "name": "meta.scalar.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-literal-content", + "begin": "$", + "while": "\\G", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "//": "Find the highest indented line", + "begin": "\\G( ++)$", + "while": "\\G(?>(\\1)$|(?!\\1)( *+)($|.))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-b-nb-literal-next", + "//": [ + "Funky wrapper function", + "The `end` pattern clears the parent `\\G` anchor", + "Affectively forcing this rule to only match at most once", + "https://github.com/microsoft/vscode-textmate/issues/114" + ], + "begin": "\\G(?!$)(?=( *+))", + "end": "\\G(?!\\1)(?=[\t ]*+#)", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-nb-literal-text", + "begin": "\\G( *+)", + "while": "\\G(?>(\\1)|( *+)($|[^\t#]|[\t ]++[^#]))", + "captures": { + "1": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "2": { + "name": "punctuation.whitespace.indentation.yaml" + }, + "3": { + "name": "invalid.illegal.expected-indentation.yaml" + } + }, + "contentName": "string.unquoted.block.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-chomped-empty", + "begin": "(?!\\G)(?=[\t ]*+#)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + { + "comment": "Header Comment", + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + } + ] + }, + "block-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-OUT)", + "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", + "while": "\\G", + "patterns": [ + { + "begin": "\\G", + "end": "(?=(?>[\t ]++|\\G)#)", + "name": "string.unquoted.plain.out.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": ":(?=[\r\n\t ])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-node": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-seq-entry (FLOW-IN)", + "patterns": [ + { + "begin": "(?=\\[|{)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "#alias" + }, + { + "begin": "(?=\"|')", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "#single" + } + ] + }, + { + "include": "#flow-plain-in" + }, + { + "include": "#presentation-detail" + } + ] + }, + "flow-mapping": { + "comment": "https://yaml.org/spec/1.2.2/#742-flow-mappings", + "begin": "{", + "end": "}", + "beginCaptures": { + "0": { + "name": "punctuation.definition.mapping.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.mapping.end.yaml" + } + }, + "name": "meta.flow.mapping.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-map-entries", + "begin": "(?<={)\\G(?=[\r\n\t ,#])|,", + "end": "(?=[^\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.mapping.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-sequence": { + "comment": "https://yaml.org/spec/1.2.2/#741-flow-sequences", + "begin": "\\[", + "end": "]", + "beginCaptures": { + "0": { + "name": "punctuation.definition.sequence.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.sequence.end.yaml" + } + }, + "name": "meta.flow.sequence.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-flow-seq-entries", + "begin": "(?<=\\[)\\G(?=[\r\n\t ,#])|,", + "end": "(?=[^\r\n\t ,#])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.sequence.yaml" + } + }, + "patterns": [ + { + "match": ",++", + "name": "invalid.illegal.separator.sequence.yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "include": "#flow-sequence-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + "flow-mapping-map-key": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}])))", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#flow-key-plain-in" + }, + { + "match": ":(?=\\[|{)", + "name": "invalid.illegal.separator.map.yaml" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?=\"|')", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-sequence-map-key": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-entry (FLOW-IN)", + "patterns": [ + { + "begin": "\\?(?=[\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\[\\]{}])", + "beginCaptures": { + "0": { + "name": "punctuation.definition.map.key.yaml" + } + }, + "name": "meta.flow.map.explicit.yaml", + "patterns": [ + { + "include": "#flow-mapping-map-key" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#flow-node" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-flow-map-implicit-entry (FLOW-IN)", + "begin": "(?<=[\t ,\\[{]|^)(?=(?>[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}]))(?>[^:#,\\[\\]{}]++|:(?![\r\n\t ,\\[\\]{}])|(?\"(?>[^\\\\\"]++|\\\\.)*+\"|'(?>[^']++|'')*+')[\t ]*+:)", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "#key-double" + }, + { + "include": "#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "#presentation-detail" + } + ] + } + ] + }, + "flow-map-value-yaml": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": ":(?=[\r\n\t ,\\[\\]{}])", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-map-value-json": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-flow-map-separate-value (FLOW-IN)", + "begin": "(?<=(?>[\"'\\]}]|^)[\t ]*+):", + "end": "(?=[,\\]}])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.flow.pair.value.yaml", + "patterns": [ + { + "include": "#flow-node" + } + ] + }, + "flow-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-multi-line (FLOW-IN)", + "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ,\\[\\]{}]))", + "end": "(?=(?>[\t ]++|\\G)#|[\t ]*+[,\\[\\]{}])", + "name": "string.unquoted.plain.in.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": ":(?=[\r\n\t ,\\[\\]{}])", + "name": "invalid.illegal.multiline-key.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "flow-key-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-plain-one-line (FLOW-OUT)", + "begin": "(?=[\\x{85}[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]|[?:-](?![\r\n\t ]))", + "end": "(?=[\t ]*+:[\r\n\t ]|[\t ]++#)", + "name": "meta.map.key.yaml string.unquoted.plain.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-out" + }, + { + "match": "\\G[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "[\t ]++$", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "flow-key-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#rule-ns-s-implicit-yaml-key (FLOW-KEY)", + "begin": "\\G(?![\r\n\t #])", + "end": "(?=[\t ]*+(?>:[\r\n\t ,\\[\\]{}]|[,\\[\\]{}])|[\t ]++#)", + "name": "meta.flow.map.key.yaml string.unquoted.plain.in.yaml entity.name.tag.yaml", + "patterns": [ + { + "include": "#tag-implicit-plain-in" + }, + { + "match": "\\x{FEFF}", + "name": "invalid.illegal.bom.yaml" + }, + { + "include": "#non-printable" + } + ] + }, + "key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", + "begin": "\\G\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.double.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "include": "#double-escape" + } + ] + }, + "key-single": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (BLOCK-KEY)", + "begin": "\\G'", + "end": "'(?!')", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", + "patterns": [ + { + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" + }, + { + "match": "''", + "name": "constant.character.escape.single-quote.yaml" + } + ] + }, + "double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.yaml" + } + }, + "name": "string.quoted.double.yaml", + "patterns": [ + { + "match": "(?x[^\"]{2,0}|u[^\"]{4,0}|U[^\"]{8,0}|.)", + "name": "invalid.illegal.constant.character.escape.yaml" + } + ] + }, + "tag-implicit-plain-in": { + "comment": "https://yaml.org/spec/1.2.2/#103-core-schema", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[+-]?+[0-9]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G0o[0-7]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G0x[0-9a-fA-F]++(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[+-]?+(?>\\.[0-9]++|[0-9]++(?>\\.[0-9]*+)?+)(?>[eE][+-]?+[0-9]++)?+(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.yaml" + }, + { + "match": "\\G[+-]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>[\r\n,\\]}]|:[\r\n\t ,\\[\\]{}]))", + "name": "constant.numeric.float.nan.yaml" + } + ] + }, + "tag-implicit-plain-out": { + "comment": "https://yaml.org/spec/1.2.2/#103-core-schema", + "patterns": [ + { + "match": "\\G(?>null|Null|NULL|~)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.language.null.yaml" + }, + { + "match": "\\G(?>true|True|TRUE|false|False|FALSE)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.language.boolean.yaml" + }, + { + "match": "\\G[+-]?+[0-9]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.integer.decimal.yaml" + }, + { + "match": "\\G0o[0-7]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.integer.octal.yaml" + }, + { + "match": "\\G0x[0-9a-fA-F]++(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.integer.hexadecimal.yaml" + }, + { + "match": "\\G[+-]?+(?>\\.[0-9]++|[0-9]++(?>\\.[0-9]*+)?+)(?>[eE][+-]?+[0-9]++)?+(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.float.yaml" + }, + { + "match": "\\G[+-]?+\\.(?>inf|Inf|INF)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.float.inf.yaml" + }, + { + "match": "\\G\\.(?>nan|NaN|NAN)(?=[\t ]++#|[\t ]*+(?>$|:[\r\n\t ]))", + "name": "constant.numeric.float.nan.yaml" + } + ] + }, + "tag-property": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-tag-property", + "//": [ + "!", + "!!", + "!<>", + "!...", + "!!...", + "!<...>", + "!...!..." + ], + "patterns": [ + { + "match": "!(?=[\r\n\t ])", + "name": "storage.type.tag.non-specific.yaml punctuation.definition.tag.non-specific.yaml" + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-verbatim-tag", + "begin": "!<", + "end": ">", + "beginCaptures": { + "0": { + "name": "punctuation.definition.tag.begin.yaml" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.tag.end.yaml" + } + }, + "name": "storage.type.tag.verbatim.yaml", + "patterns": [ + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "#non-printable" + }, + { + "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$,_.!~*'()\\[\\]%>]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-ns-shorthand-tag", + "begin": "(?=!)", + "end": "(?=[\r\n\t ,\\[\\]{}])", + "name": "storage.type.tag.shorthand.yaml", + "patterns": [ + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", + "match": "\\G!!", + "name": "punctuation.definition.tag.secondary.yaml" + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-secondary-tag-handle", + "match": "\\G(!)[0-9A-Za-z-]++(!)", + "captures": { + "1": { + "name": "punctuation.definition.tag.named.yaml" + }, + "2": { + "name": "punctuation.definition.tag.named.yaml" + } + } + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-primary-tag-handle", + "match": "\\G!", + "name": "punctuation.definition.tag.primary.yaml" + }, + { + "match": "%[0-9a-fA-F]{2}", + "name": "constant.character.escape.unicode.8-bit.yaml" + }, + { + "match": "%[^\r\n\t ]{2,0}", + "name": "invalid.illegal.constant.character.escape.unicode.8-bit.yaml" + }, + { + "include": "#non-printable" + }, + { + "match": "[^\r\n\t a-zA-Z0-9-#;/?:@&=+$_.~*'()%]++", + "name": "invalid.illegal.unrecognized.yaml" + } + ] + } + ] + }, + "anchor-property": { + "match": "(&)([\\x{85}[^ ,\\[\\]{}\\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)|(&)", + "captures": { + "0": { + "name": "keyword.control.flow.anchor.yaml" + }, + "1": { + "name": "punctuation.definition.anchor.yaml" + }, + "2": { + "name": "variable.other.anchor.yaml" + }, + "3": { + "name": "invalid.illegal.flow.anchor.yaml" + } + } + }, + "alias": { + "begin": "(\\*)([\\x{85}[^ ,\\[\\]{}\\p{Cntrl}\\p{Surrogate}\\x{FEFF FFFE FFFF}]]++)|(\\*)", + "end": "(?=:[\r\n\t ,\\[\\]{}]|[,\\[\\]{}])", + "captures": { + "0": { + "name": "keyword.control.flow.alias.yaml" + }, + "1": { + "name": "punctuation.definition.alias.yaml" + }, + "2": { + "name": "variable.other.alias.yaml" + }, + "3": { + "name": "invalid.illegal.flow.alias.yaml" + } + }, + "patterns": [ + { + "include": "#presentation-detail" + } + ] + }, + "byte-order-mark": { + "comment": "", + "match": "\\G\\x{FEFF}++", + "name": "byte-order-mark.yaml" + }, + "presentation-detail": { + "patterns": [ + { + "match": "[\t ]++", + "name": "punctuation.whitespace.separator.yaml" + }, + { + "include": "#non-printable" + }, + { + "include": "#comment" + }, + { + "include": "#unknown" + } + ] + }, + "non-printable": { + "//": { + "85": "Â…", + "10000": "ð€€", + "A0": " ", + "D7FF": "퟿", + "E000": "", + "FFFD": "�", + "FEFF": "", + "FFFF": "ï¿¿", + "10FFFF": "ô¿¿" + }, + "//match": "[\\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}&&[^\t\n\r\\x{85}]]++", + "match": "[^\t\n\r -~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]++", + "name": "invalid.illegal.non-printable.yaml" + }, + "comment": { + "comment": "Comments must be separated from other tokens by white space characters. `space`, `tab`, `newline` or `carriage-return`. `#(.*)` causes performance issues", + "begin": "(?<=[\\x{FEFF}\t ]|^)#", + "end": "\r|\n", + "captures": { + "0": { + "name": "punctuation.definition.comment.yaml" + } + }, + "name": "comment.line.number-sign.yaml", + "patterns": [ + { + "include": "#non-printable" + } + ] + }, + "unknown": { + "match": ".[[^\"':,\\[\\]{}]&&!-~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", + "name": "invalid.illegal.unrecognized.yaml markup.strikethrough" + } + } +} \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml.tmLanguage.json b/extensions/yaml/syntaxes/yaml.tmLanguage.json index 447df713901..39d8e586442 100644 --- a/extensions/yaml/syntaxes/yaml.tmLanguage.json +++ b/extensions/yaml/syntaxes/yaml.tmLanguage.json @@ -1,621 +1,21 @@ { "information_for_contributors": [ - "This file has been converted from https://github.com/textmate/yaml.tmbundle/blob/master/Syntaxes/YAML.tmLanguage", + "This file has been converted from https://github.com/RedCMD/YAML-Syntax-Highlighter/blob/master/syntaxes/yaml.tmLanguage.json", "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/textmate/yaml.tmbundle/commit/e54ceae3b719506dba7e481a77cea4a8b576ae46", - "name": "YAML", + "version": "https://github.com/RedCMD/YAML-Syntax-Highlighter/commit/5d2a15e2ee4bb9c2cc9a86a0b72aea8fa2aba1e1", + "name": "YAML Ain't Markup Language", "scopeName": "source.yaml", "patterns": [ { - "include": "#comment" - }, - { - "include": "#property" - }, - { - "include": "#directive" - }, - { - "match": "^---", - "name": "entity.other.document.begin.yaml" - }, - { - "match": "^\\.{3}", - "name": "entity.other.document.end.yaml" - }, - { - "include": "#node" + "comment": "Default to YAML version 1.2", + "include": "source.yaml.1.2" } ], "repository": { - "block-collection": { - "patterns": [ - { - "include": "#block-sequence" - }, - { - "include": "#block-mapping" - } - ] - }, - "block-mapping": { - "patterns": [ - { - "include": "#block-pair" - } - ] - }, - "block-node": { - "patterns": [ - { - "include": "#prototype" - }, - { - "include": "#block-scalar" - }, - { - "include": "#block-collection" - }, - { - "include": "#flow-scalar-plain-out" - }, - { - "include": "#flow-node" - } - ] - }, - "block-pair": { - "patterns": [ - { - "begin": "\\?", - "beginCaptures": { - "1": { - "name": "punctuation.definition.key-value.begin.yaml" - } - }, - "end": "(?=\\?)|^ *(:)|(:)", - "endCaptures": { - "1": { - "name": "punctuation.separator.key-value.mapping.yaml" - }, - "2": { - "name": "invalid.illegal.expected-newline.yaml" - } - }, - "name": "meta.block-mapping.yaml", - "patterns": [ - { - "include": "#block-node" - } - ] - }, - { - "begin": "(?x)\n (?=\n (?x:\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n )\n (\n [^\\s:]\n | : \\S\n | \\s+ (?![#\\s])\n )*\n \\s*\n :\n\t\t\t\t\t\t\t(\\s|$)\n )\n ", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", - "patterns": [ - { - "include": "#flow-scalar-plain-out-implicit-type" - }, - { - "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n ", - "beginCaptures": { - "0": { - "name": "entity.name.tag.yaml" - } - }, - "contentName": "entity.name.tag.yaml", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", - "name": "string.unquoted.plain.out.yaml" - } - ] - }, - { - "match": ":(?=\\s|$)", - "name": "punctuation.separator.key-value.mapping.yaml" - } - ] - }, - "block-scalar": { - "begin": "(?:(\\|)|(>))([1-9])?([-+])?(.*\\n?)", - "beginCaptures": { - "1": { - "name": "keyword.control.flow.block-scalar.literal.yaml" - }, - "2": { - "name": "keyword.control.flow.block-scalar.folded.yaml" - }, - "3": { - "name": "constant.numeric.indentation-indicator.yaml" - }, - "4": { - "name": "storage.modifier.chomping-indicator.yaml" - }, - "5": { - "patterns": [ - { - "include": "#comment" - }, - { - "match": ".+", - "name": "invalid.illegal.expected-comment-or-newline.yaml" - } - ] - } - }, - "end": "^(?=\\S)|(?!\\G)", - "patterns": [ - { - "begin": "^([ ]+)(?! )", - "end": "^(?!\\1|\\s*$)", - "name": "string.unquoted.block.yaml" - } - ] - }, - "block-sequence": { - "match": "(-)(?!\\S)", - "name": "punctuation.definition.block.sequence.item.yaml" - }, - "comment": { - "begin": "(?:(^[ \\t]*)|[ \\t]+)(?=#\\p{Print}*$)", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.comment.leading.yaml" - } - }, - "end": "(?!\\G)", - "patterns": [ - { - "begin": "#", - "beginCaptures": { - "0": { - "name": "punctuation.definition.comment.yaml" - } - }, - "end": "\\n", - "name": "comment.line.number-sign.yaml" - } - ] - }, - "directive": { - "begin": "^%", - "beginCaptures": { - "0": { - "name": "punctuation.definition.directive.begin.yaml" - } - }, - "end": "(?=$|[ \\t]+($|#))", - "name": "meta.directive.yaml", - "patterns": [ - { - "captures": { - "1": { - "name": "keyword.other.directive.yaml.yaml" - }, - "2": { - "name": "constant.numeric.yaml-version.yaml" - } - }, - "match": "\\G(YAML)[ \\t]+(\\d+\\.\\d+)" - }, - { - "captures": { - "1": { - "name": "keyword.other.directive.tag.yaml" - }, - "2": { - "name": "storage.type.tag-handle.yaml" - }, - "3": { - "name": "support.type.tag-prefix.yaml" - } - }, - "match": "(?x)\n \\G\n (TAG)\n (?:[ \\t]+\n ((?:!(?:[0-9A-Za-z\\-]*!)?))\n (?:[ \\t]+ (\n ! (?x: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )*\n | (?![,!\\[\\]{}]) (?x: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )+\n )\n )?\n )?\n " - }, - { - "captures": { - "1": { - "name": "support.other.directive.reserved.yaml" - }, - "2": { - "name": "string.unquoted.directive-name.yaml" - }, - "3": { - "name": "string.unquoted.directive-parameter.yaml" - } - }, - "match": "(?x) \\G (\\w+) (?:[ \\t]+ (\\w+) (?:[ \\t]+ (\\w+))? )?" - }, - { - "match": "\\S+", - "name": "invalid.illegal.unrecognized.yaml" - } - ] - }, - "flow-alias": { - "captures": { - "1": { - "name": "keyword.control.flow.alias.yaml" - }, - "2": { - "name": "punctuation.definition.alias.yaml" - }, - "3": { - "name": "variable.other.alias.yaml" - }, - "4": { - "name": "invalid.illegal.character.anchor.yaml" - } - }, - "match": "((\\*))([^\\s\\[\\]/{/},]+)([^\\s\\]},]\\S*)?" - }, - "flow-collection": { - "patterns": [ - { - "include": "#flow-sequence" - }, - { - "include": "#flow-mapping" - } - ] - }, - "flow-mapping": { - "begin": "\\{", - "beginCaptures": { - "0": { - "name": "punctuation.definition.mapping.begin.yaml" - } - }, - "end": "\\}", - "endCaptures": { - "0": { - "name": "punctuation.definition.mapping.end.yaml" - } - }, - "name": "meta.flow-mapping.yaml", - "patterns": [ - { - "include": "#prototype" - }, - { - "match": ",", - "name": "punctuation.separator.mapping.yaml" - }, - { - "include": "#flow-pair" - } - ] - }, - "flow-node": { - "patterns": [ - { - "include": "#prototype" - }, - { - "include": "#flow-alias" - }, - { - "include": "#flow-collection" - }, - { - "include": "#flow-scalar" - } - ] - }, - "flow-pair": { - "patterns": [ - { - "begin": "\\?", - "beginCaptures": { - "0": { - "name": "punctuation.definition.key-value.begin.yaml" - } - }, - "end": "(?=[},\\]])", - "name": "meta.flow-pair.explicit.yaml", - "patterns": [ - { - "include": "#prototype" - }, - { - "include": "#flow-pair" - }, - { - "include": "#flow-node" - }, - { - "begin": ":(?=\\s|$|[\\[\\]{},])", - "beginCaptures": { - "0": { - "name": "punctuation.separator.key-value.mapping.yaml" - } - }, - "end": "(?=[},\\]])", - "patterns": [ - { - "include": "#flow-value" - } - ] - } - ] - }, - { - "begin": "(?x)\n (?=\n (?:\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n )\n (\n [^\\s:[\\[\\]{},]]\n | : [^\\s[\\[\\]{},]]\n | \\s+ (?![#\\s])\n )*\n \\s*\n :\n\t\t\t\t\t\t\t(\\s|$)\n )\n ", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", - "name": "meta.flow-pair.key.yaml", - "patterns": [ - { - "include": "#flow-scalar-plain-in-implicit-type" - }, - { - "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n ", - "beginCaptures": { - "0": { - "name": "entity.name.tag.yaml" - } - }, - "contentName": "entity.name.tag.yaml", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", - "name": "string.unquoted.plain.in.yaml" - } - ] - }, - { - "include": "#flow-node" - }, - { - "begin": ":(?=\\s|$|[\\[\\]{},])", - "captures": { - "0": { - "name": "punctuation.separator.key-value.mapping.yaml" - } - }, - "end": "(?=[},\\]])", - "name": "meta.flow-pair.yaml", - "patterns": [ - { - "include": "#flow-value" - } - ] - } - ] - }, - "flow-scalar": { - "patterns": [ - { - "include": "#flow-scalar-double-quoted" - }, - { - "include": "#flow-scalar-single-quoted" - }, - { - "include": "#flow-scalar-plain-in" - } - ] - }, - "flow-scalar-double-quoted": { - "begin": "\"", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "end": "\"", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "string.quoted.double.yaml", - "patterns": [ - { - "match": "\\\\([0abtnvfre \"/\\\\N_Lp]|x\\d\\d|u\\d{4}|U\\d{8})", - "name": "constant.character.escape.yaml" - }, - { - "match": "\\\\\\n", - "name": "constant.character.escape.double-quoted.newline.yaml" - } - ] - }, - "flow-scalar-plain-in": { - "patterns": [ - { - "include": "#flow-scalar-plain-in-implicit-type" - }, - { - "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n ", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", - "name": "string.unquoted.plain.in.yaml" - } - ] - }, - "flow-scalar-plain-in-implicit-type": { - "patterns": [ - { - "captures": { - "1": { - "name": "constant.language.null.yaml" - }, - "2": { - "name": "constant.language.boolean.yaml" - }, - "3": { - "name": "constant.numeric.integer.yaml" - }, - "4": { - "name": "constant.numeric.float.yaml" - }, - "5": { - "name": "constant.other.timestamp.yaml" - }, - "6": { - "name": "constant.language.value.yaml" - }, - "7": { - "name": "constant.language.merge.yaml" - } - }, - "match": "(?x)\n (?x:\n (null|Null|NULL|~)\n | (y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)\n | (\n (?:\n [-+]? 0b [0-1_]+ # (base 2)\n | [-+]? 0 [0-7_]+ # (base 8)\n | [-+]? (?: 0|[1-9][0-9_]*) # (base 10)\n | [-+]? 0x [0-9a-fA-F_]+ # (base 16)\n | [-+]? [1-9] [0-9_]* (?: :[0-5]?[0-9])+ # (base 60)\n )\n )\n | (\n (?x:\n [-+]? (?: [0-9] [0-9_]*)? \\. [0-9.]* (?: [eE] [-+] [0-9]+)? # (base 10)\n | [-+]? [0-9] [0-9_]* (?: :[0-5]?[0-9])+ \\. [0-9_]* # (base 60)\n | [-+]? \\. (?: inf|Inf|INF) # (infinity)\n | \\. (?: nan|NaN|NAN) # (not a number)\n )\n )\n | (\n (?x:\n \\d{4} - \\d{2} - \\d{2} # (y-m-d)\n | \\d{4} # (year)\n - \\d{1,2} # (month)\n - \\d{1,2} # (day)\n (?: [Tt] | [ \\t]+) \\d{1,2} # (hour)\n : \\d{2} # (minute)\n : \\d{2} # (second)\n (?: \\.\\d*)? # (fraction)\n (?:\n (?:[ \\t]*) Z\n | [-+] \\d{1,2} (?: :\\d{1,2})?\n )? # (time zone)\n )\n )\n | (=)\n | (<<)\n )\n (?:\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n )\n " - } - ] - }, - "flow-scalar-plain-out": { - "patterns": [ - { - "include": "#flow-scalar-plain-out-implicit-type" - }, - { - "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n ", - "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", - "name": "string.unquoted.plain.out.yaml" - } - ] - }, - "flow-scalar-plain-out-implicit-type": { - "patterns": [ - { - "captures": { - "1": { - "name": "constant.language.null.yaml" - }, - "2": { - "name": "constant.language.boolean.yaml" - }, - "3": { - "name": "constant.numeric.integer.yaml" - }, - "4": { - "name": "constant.numeric.float.yaml" - }, - "5": { - "name": "constant.other.timestamp.yaml" - }, - "6": { - "name": "constant.language.value.yaml" - }, - "7": { - "name": "constant.language.merge.yaml" - } - }, - "match": "(?x)\n (?x:\n (null|Null|NULL|~)\n | (y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)\n | (\n (?:\n [-+]? 0b [0-1_]+ # (base 2)\n | [-+]? 0 [0-7_]+ # (base 8)\n | [-+]? (?: 0|[1-9][0-9_]*) # (base 10)\n | [-+]? 0x [0-9a-fA-F_]+ # (base 16)\n | [-+]? [1-9] [0-9_]* (?: :[0-5]?[0-9])+ # (base 60)\n )\n )\n | (\n (?x:\n [-+]? (?: [0-9] [0-9_]*)? \\. [0-9.]* (?: [eE] [-+] [0-9]+)? # (base 10)\n | [-+]? [0-9] [0-9_]* (?: :[0-5]?[0-9])+ \\. [0-9_]* # (base 60)\n | [-+]? \\. (?: inf|Inf|INF) # (infinity)\n | \\. (?: nan|NaN|NAN) # (not a number)\n )\n )\n | (\n (?x:\n \\d{4} - \\d{2} - \\d{2} # (y-m-d)\n | \\d{4} # (year)\n - \\d{1,2} # (month)\n - \\d{1,2} # (day)\n (?: [Tt] | [ \\t]+) \\d{1,2} # (hour)\n : \\d{2} # (minute)\n : \\d{2} # (second)\n (?: \\.\\d*)? # (fraction)\n (?:\n (?:[ \\t]*) Z\n | [-+] \\d{1,2} (?: :\\d{1,2})?\n )? # (time zone)\n )\n )\n | (=)\n | (<<)\n )\n (?x:\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n )\n " - } - ] - }, - "flow-scalar-single-quoted": { - "begin": "'", - "beginCaptures": { - "0": { - "name": "punctuation.definition.string.begin.yaml" - } - }, - "end": "'(?!')", - "endCaptures": { - "0": { - "name": "punctuation.definition.string.end.yaml" - } - }, - "name": "string.quoted.single.yaml", - "patterns": [ - { - "match": "''", - "name": "constant.character.escape.single-quoted.yaml" - } - ] - }, - "flow-sequence": { - "begin": "\\[", - "beginCaptures": { - "0": { - "name": "punctuation.definition.sequence.begin.yaml" - } - }, - "end": "\\]", - "endCaptures": { - "0": { - "name": "punctuation.definition.sequence.end.yaml" - } - }, - "name": "meta.flow-sequence.yaml", - "patterns": [ - { - "include": "#prototype" - }, - { - "match": ",", - "name": "punctuation.separator.sequence.yaml" - }, - { - "include": "#flow-pair" - }, - { - "include": "#flow-node" - } - ] - }, - "flow-value": { - "patterns": [ - { - "begin": "\\G(?![},\\]])", - "end": "(?=[},\\]])", - "name": "meta.flow-pair.value.yaml", - "patterns": [ - { - "include": "#flow-node" - } - ] - } - ] - }, - "node": { - "patterns": [ - { - "include": "#block-node" - } - ] - }, - "property": { - "begin": "(?=!|&)", - "end": "(?!\\G)", - "name": "meta.property.yaml", - "patterns": [ - { - "captures": { - "1": { - "name": "keyword.control.property.anchor.yaml" - }, - "2": { - "name": "punctuation.definition.anchor.yaml" - }, - "3": { - "name": "entity.name.type.anchor.yaml" - }, - "4": { - "name": "invalid.illegal.character.anchor.yaml" - } - }, - "match": "\\G((&))([^\\s\\[\\]/{/},]+)(\\S+)?" - }, - { - "match": "(?x)\n \\G\n (?:\n ! < (?: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )+ >\n | (?:!(?:[0-9A-Za-z\\-]*!)?) (?: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$_.~*'()] )+\n | !\n )\n (?=\\ |\\t|$)\n ", - "name": "storage.type.tag-handle.yaml" - }, - { - "match": "\\S+", - "name": "invalid.illegal.tag-handle.yaml" - } - ] - }, - "prototype": { - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#property" - } - ] + "parity": { + "comment": "Yes... That is right. Due to the changes with \\x2028, \\x2029, \\x85 and 'tags'. This is all the code I was able to reuse between all versions 1.3, 1.2, 1.1 and 1.0" } } } \ No newline at end of file diff --git a/extensions/yarn.lock b/extensions/yarn.lock index fa4595ffa74..b981143bdd0 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -128,11 +128,11 @@ node-gyp-build "^4.3.0" braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" coffeescript@1.12.7: version "1.12.7" @@ -180,10 +180,10 @@ fast-plist@0.1.2: resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg= -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -234,10 +234,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -typescript@5.4.5: - version "5.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@^5.5.2: + version "5.5.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507" + integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew== vscode-grammar-updater@^1.1.0: version "1.1.0" diff --git a/package.json b/package.json index 07f7bae6692..7a7ca63dd80 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.90.0", - "distro": "0f8d3c619e9a416854972b1f496b1eedc727ab18", + "version": "1.92.0", + "distro": "58e7d90e05684b6937db21dd372f7a088bdc9dc1", "author": { "name": "Microsoft Corporation" }, @@ -52,7 +52,7 @@ "watch-cli": "node --max-old-space-size=4095 ./node_modules/gulp/bin/gulp.js watch-cli", "eslint": "node build/eslint", "stylelint": "node build/stylelint", - "playwright-install": "node build/azure-pipelines/common/installPlaywright.js", + "playwright-install": "yarn playwright install", "compile-build": "node --max-old-space-size=4095 ./node_modules/gulp/bin/gulp.js compile-build", "compile-extensions-build": "node --max-old-space-size=4095 ./node_modules/gulp/bin/gulp.js compile-extensions-build", "minify-vscode": "node --max-old-space-size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode", @@ -73,7 +73,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", - "@vscode/proxy-agent": "^0.19.0", + "@vscode/proxy-agent": "^0.21.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.6-vscode", @@ -82,23 +82,24 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-image": "0.9.0-beta.17", - "@xterm/addon-search": "0.16.0-beta.17", - "@xterm/addon-serialize": "0.14.0-beta.17", - "@xterm/addon-unicode11": "0.9.0-beta.17", - "@xterm/addon-webgl": "0.19.0-beta.17", - "@xterm/headless": "5.6.0-beta.17", - "@xterm/xterm": "5.6.0-beta.17", - "graceful-fs": "4.2.11", + "@xterm/addon-clipboard": "0.2.0-beta.19", + "@xterm/addon-image": "0.9.0-beta.36", + "@xterm/addon-search": "0.16.0-beta.36", + "@xterm/addon-serialize": "0.14.0-beta.36", + "@xterm/addon-unicode11": "0.9.0-beta.36", + "@xterm/addon-webgl": "0.19.0-beta.36", + "@xterm/headless": "5.6.0-beta.36", + "@xterm/xterm": "5.6.0-beta.36", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", - "jschardet": "3.0.0", + "jschardet": "3.1.3", "kerberos": "^2.0.1", "minimist": "^1.2.6", "native-is-elevated": "0.7.0", "native-keymap": "^3.3.5", "native-watchdog": "^1.4.1", "node-pty": "1.1.0-beta11", + "open": "^8.4.2", "tas-client-umd": "0.2.0", "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", @@ -108,11 +109,10 @@ "yazl": "^2.4.3" }, "devDependencies": { - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.45.0", "@swc/core": "1.3.62", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", - "@types/graceful-fs": "4.1.2", "@types/gulp-svgmin": "^1.2.1", "@types/http-proxy-agent": "^2.0.1", "@types/kerberos": "^1.1.2", @@ -137,7 +137,7 @@ "@vscode/telemetry-extractor": "^1.10.2", "@vscode/test-cli": "^0.0.6", "@vscode/test-electron": "^2.3.8", - "@vscode/test-web": "^0.0.50", + "@vscode/test-web": "^0.0.56", "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.14", "@webgpu/types": "^0.1.40", @@ -209,7 +209,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.5.0-dev.20240521", + "typescript": "^5.6.0-dev.20240703", "util": "^0.12.4", "vscode-nls-dev": "^3.3.1", "webpack": "^5.91.0", diff --git a/product.json b/product.json index ad46ae24e97..27ae53fe16b 100644 --- a/product.json +++ b/product.json @@ -50,8 +50,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.89.0", - "sha256": "2abd9c01f711c0d60d40b722b5bed0930701be173931f3c6251906f692858221", + "version": "1.91.0", + "sha256": "53b99146c7fa280f00c74414e09721530c622bf3e5eac2c967ddfb9906b51c80", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", diff --git a/remote/.yarnrc b/remote/.yarnrc index 3a01071e2ba..4c99388e889 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,5 +1,5 @@ disturl "https://nodejs.org/dist" -target "20.9.0" -ms_build_id "274207" +target "20.11.1" +ms_build_id "275039" runtime "node" build_from_source "true" diff --git a/remote/package.json b/remote/package.json index 3cdd8501efd..bb615b88ea4 100644 --- a/remote/package.json +++ b/remote/package.json @@ -6,25 +6,26 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", + "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.19.0", + "@vscode/proxy-agent": "^0.21.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-image": "0.9.0-beta.17", - "@xterm/addon-search": "0.16.0-beta.17", - "@xterm/addon-serialize": "0.14.0-beta.17", - "@xterm/addon-unicode11": "0.9.0-beta.17", - "@xterm/addon-webgl": "0.19.0-beta.17", - "@xterm/headless": "5.6.0-beta.17", - "@xterm/xterm": "5.6.0-beta.17", + "@xterm/addon-clipboard": "0.2.0-beta.19", + "@xterm/addon-image": "0.9.0-beta.36", + "@xterm/addon-search": "0.16.0-beta.36", + "@xterm/addon-serialize": "0.14.0-beta.36", + "@xterm/addon-unicode11": "0.9.0-beta.36", + "@xterm/addon-webgl": "0.19.0-beta.36", + "@xterm/headless": "5.6.0-beta.36", + "@xterm/xterm": "5.6.0-beta.36", "cookie": "^0.4.0", - "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", - "jschardet": "3.0.0", + "jschardet": "3.1.3", "kerberos": "^2.0.1", "minimist": "^1.2.6", "native-watchdog": "^1.4.1", diff --git a/remote/web/package.json b/remote/web/package.json index 76375094a7d..f6b57db5ea0 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -7,13 +7,14 @@ "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-image": "0.9.0-beta.17", - "@xterm/addon-search": "0.16.0-beta.17", - "@xterm/addon-serialize": "0.14.0-beta.17", - "@xterm/addon-unicode11": "0.9.0-beta.17", - "@xterm/addon-webgl": "0.19.0-beta.17", - "@xterm/xterm": "5.6.0-beta.17", - "jschardet": "3.0.0", + "@xterm/addon-clipboard": "0.2.0-beta.19", + "@xterm/addon-image": "0.9.0-beta.36", + "@xterm/addon-search": "0.16.0-beta.36", + "@xterm/addon-serialize": "0.14.0-beta.36", + "@xterm/addon-unicode11": "0.9.0-beta.36", + "@xterm/addon-webgl": "0.19.0-beta.36", + "@xterm/xterm": "5.6.0-beta.36", + "jschardet": "3.1.3", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-textmate": "9.0.0" diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 81215235ac6..a28d00372f7 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -48,40 +48,52 @@ resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== -"@xterm/addon-image@0.9.0-beta.17": - version "0.9.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.17.tgz#343d0665a6060d4f893b4f2d32de6ccbbd00bb63" - integrity sha512-g0r2hpBcLABY5as4llsMP36RHtkWooEn7tf+7U0/hTndJoCAvs4uGDqZNQigFgeAM3lJ4PnRYh4lfnEh9bGt8A== +"@xterm/addon-clipboard@0.2.0-beta.19": + version "0.2.0-beta.19" + resolved "https://registry.yarnpkg.com/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.19.tgz#da2ea7a0d6e51383d4a21cbb04fb7fbd9db7d853" + integrity sha512-A/NxJQoOq21kE1ykZ07Cw3IxD5cQFxba1iMxnSFvWGVC71ZdHGwUveLeY8nHWEL8PfLsZxAgIzlMTfWgfkQ+CA== + dependencies: + js-base64 "^3.7.5" -"@xterm/addon-search@0.16.0-beta.17": - version "0.16.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.17.tgz#7cb01c7f498405909d37040884ee22d1889a36d2" - integrity sha512-wBfxmWOeqG6HHHE5mVamDJ75zBdHC35ERNy5/aTpQsQsyxrnV0Ks76c8ZVTaTu9wyBCAyx7UmZT42Ot80khY/g== +"@xterm/addon-image@0.9.0-beta.36": + version "0.9.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.36.tgz#79024103c48f4e401ca15afe49fad4f3834c023c" + integrity sha512-m8c5OfJBzPYfv90mSgc0bX/P+qUsgczVajHW+kE59UoC311ng13IlCg6a4bJHb2EHqGsq19fIrYCn6+JsMdRsQ== -"@xterm/addon-serialize@0.14.0-beta.17": - version "0.14.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.17.tgz#1cb8e35c0d118060a807adb340624fa7f80dd9c5" - integrity sha512-/c3W39kdRgGGYDoYjXb5HrUC421qwPn6NryAT4WJuJWnyMtFbe2DPwKsTfHuCBPiPyovS3a9j950Md3O3YXDZA== +"@xterm/addon-search@0.16.0-beta.36": + version "0.16.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.36.tgz#22deda3250552f24de05f8112299d15f3fe90f01" + integrity sha512-lN66vYpKvNBxbvtJXLbuidirirmIzySXnl8JvarcrDaw4HlqluOvvjEdVYKofWV5ZGSaPfIAijwJW1f0KjUhJw== -"@xterm/addon-unicode11@0.9.0-beta.17": - version "0.9.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.17.tgz#b5558148029a796c6a6d78e2a8b7255f92a51530" - integrity sha512-z7v8uojFVrO1aLSWtnz5MzSrfWRT8phde7kh9ufqHLBv7YYtMHxlPVjSuW8PZ2h4eY1LOZf6icUAzrmyJmJ7Kg== +"@xterm/addon-serialize@0.14.0-beta.36": + version "0.14.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.36.tgz#1407c13fe1bd869ad4f26e7b7da4e7fa87442021" + integrity sha512-6KpzHlQIuHakPv70dKhQp8f6e9hk4q1fNuuTD1rEzDg8DeKRfUDjorw1vPkKTB/DD+3zaMUBtg7DFVVEi+/+Cw== -"@xterm/addon-webgl@0.19.0-beta.17": - version "0.19.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.17.tgz#68ad9e68dd1cf581b391971de33f5c04966b0d8e" - integrity sha512-X8ObRgoZl7UZTgdndM+mpSO3hLzAhWKoXXrGvUQg/7XabRKAPrQ2XvdyZm04nYwibE6Tpit2h5kkxjlVqupIig== +"@xterm/addon-unicode11@0.9.0-beta.36": + version "0.9.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.36.tgz#158dcdd707a466958a256a960e5d9a967a97a9dc" + integrity sha512-BKP2ml0fYOHnfaTp0LorSluNXjHRSEwf3yrD3K6jEZfYTBePhee1TAxOdNH/TdqwNYZYaYHaK87A5mSuYpKPBQ== -"@xterm/xterm@5.6.0-beta.17": - version "5.6.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.17.tgz#67ce2e2ff45bd6cc9f26d455d5522c6c4a122ed9" - integrity sha512-+wAv8PhaGQSN9yXWIa8EFtT33pbrA4lZakMB1P05fr+DQ7zoH66QOAUoDY95uOf/4+S6Ihz8wzP2+FH8zETQEA== +"@xterm/addon-webgl@0.19.0-beta.36": + version "0.19.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.36.tgz#8926a0434e5ce74eee12a965c06cd5f601391f18" + integrity sha512-bJA1enVNlIMRkBU9i7i8qX26Zs2/CrGedREW5WI0NZUAn0IHlatWlj3aOfTuI2MYWUPGE8ul30PyipYP6P+fmA== -jschardet@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" - integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +"@xterm/xterm@5.6.0-beta.36": + version "5.6.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.36.tgz#fd0fd598b67e3bcba61a59bb1a33b131ad86eea3" + integrity sha512-YtFKQIggbvV2brWifksZAtLi447j0DFdoSRoq4vQi/N7KFC0pguGdG3YzYkDOyqoeLMPu569e2b5oevMe6d2aQ== + +js-base64@^3.7.5: + version "3.7.7" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" + integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== + +jschardet@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.1.3.tgz#10c2289fdae91a0aa9de8bba9c59055fd78898d3" + integrity sha512-Q1PKVMK/uu+yjdlobgWIYkUOCR1SqUmW9m/eUJNNj4zI2N12i25v8fYpVf+zCakQeaTdBdhnZTFbVIAVZIVVOg== tas-client-umd@0.2.0: version "0.2.0" diff --git a/remote/yarn.lock b/remote/yarn.lock index 71e8b408a1f..0f65b688c17 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -53,15 +53,23 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-3.0.0.tgz#d52238c9052d746c9689523e650160e70786bc9a" integrity sha512-OAdBVB7rlwvLD+DiecSAyVKzKVmSfXbouCyM5I6wHGi4MGXIyFqErg1IvyJ7PI1e+GYZuZh7cCHV/c4LA8SKMw== +"@vscode/deviceid@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@vscode/deviceid/-/deviceid-0.1.1.tgz#750e2930a3a8fbf3fd610096a8b915dfdb493c89" + integrity sha512-ErpoMeKKNYAkR1IT3zxB5RtiTqEECdh8fxggupWvzuxpTAX77hwOI2NdJ7um+vupnXRBZVx4ugo0+dVHJWUkag== + dependencies: + fs-extra "^11.2.0" + uuid "^9.0.1" + "@vscode/iconv-lite-umd@0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== -"@vscode/proxy-agent@^0.19.0": - version "0.19.1" - resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.19.1.tgz#d9640d85df1c48885580b68bb4b2b54e17f5332c" - integrity sha512-cs1VOx6d5n69HhgzK0cWeyfudJt+9LdJi/vtgRRxxwisWKg4h83B3+EUJ4udF5SEkJgMBp3oU0jheZVt43ImnQ== +"@vscode/proxy-agent@^0.21.0": + version "0.21.0" + resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.21.0.tgz#93c818b863ad20b42679032ecc1e3ecdc6306f12" + integrity sha512-9YcpBq+ZhMr3EQY/5ScyHc9kIIU/AcYOQn3DXq0N9tl81ViVsUvii3Fh+FAtD0YQ/qWtDfGxt8VCWZtuyh2D0g== dependencies: "@tootallnate/once" "^3.0.0" agent-base "^7.0.1" @@ -114,40 +122,47 @@ resolved "https://registry.yarnpkg.com/@vscode/windows-registry/-/windows-registry-1.1.0.tgz#03dace7c29c46f658588b9885b9580e453ad21f9" integrity sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw== -"@xterm/addon-image@0.9.0-beta.17": - version "0.9.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.17.tgz#343d0665a6060d4f893b4f2d32de6ccbbd00bb63" - integrity sha512-g0r2hpBcLABY5as4llsMP36RHtkWooEn7tf+7U0/hTndJoCAvs4uGDqZNQigFgeAM3lJ4PnRYh4lfnEh9bGt8A== +"@xterm/addon-clipboard@0.2.0-beta.19": + version "0.2.0-beta.19" + resolved "https://registry.yarnpkg.com/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.19.tgz#da2ea7a0d6e51383d4a21cbb04fb7fbd9db7d853" + integrity sha512-A/NxJQoOq21kE1ykZ07Cw3IxD5cQFxba1iMxnSFvWGVC71ZdHGwUveLeY8nHWEL8PfLsZxAgIzlMTfWgfkQ+CA== + dependencies: + js-base64 "^3.7.5" -"@xterm/addon-search@0.16.0-beta.17": - version "0.16.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.17.tgz#7cb01c7f498405909d37040884ee22d1889a36d2" - integrity sha512-wBfxmWOeqG6HHHE5mVamDJ75zBdHC35ERNy5/aTpQsQsyxrnV0Ks76c8ZVTaTu9wyBCAyx7UmZT42Ot80khY/g== +"@xterm/addon-image@0.9.0-beta.36": + version "0.9.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.9.0-beta.36.tgz#79024103c48f4e401ca15afe49fad4f3834c023c" + integrity sha512-m8c5OfJBzPYfv90mSgc0bX/P+qUsgczVajHW+kE59UoC311ng13IlCg6a4bJHb2EHqGsq19fIrYCn6+JsMdRsQ== -"@xterm/addon-serialize@0.14.0-beta.17": - version "0.14.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.17.tgz#1cb8e35c0d118060a807adb340624fa7f80dd9c5" - integrity sha512-/c3W39kdRgGGYDoYjXb5HrUC421qwPn6NryAT4WJuJWnyMtFbe2DPwKsTfHuCBPiPyovS3a9j950Md3O3YXDZA== +"@xterm/addon-search@0.16.0-beta.36": + version "0.16.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.16.0-beta.36.tgz#22deda3250552f24de05f8112299d15f3fe90f01" + integrity sha512-lN66vYpKvNBxbvtJXLbuidirirmIzySXnl8JvarcrDaw4HlqluOvvjEdVYKofWV5ZGSaPfIAijwJW1f0KjUhJw== -"@xterm/addon-unicode11@0.9.0-beta.17": - version "0.9.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.17.tgz#b5558148029a796c6a6d78e2a8b7255f92a51530" - integrity sha512-z7v8uojFVrO1aLSWtnz5MzSrfWRT8phde7kh9ufqHLBv7YYtMHxlPVjSuW8PZ2h4eY1LOZf6icUAzrmyJmJ7Kg== +"@xterm/addon-serialize@0.14.0-beta.36": + version "0.14.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.36.tgz#1407c13fe1bd869ad4f26e7b7da4e7fa87442021" + integrity sha512-6KpzHlQIuHakPv70dKhQp8f6e9hk4q1fNuuTD1rEzDg8DeKRfUDjorw1vPkKTB/DD+3zaMUBtg7DFVVEi+/+Cw== -"@xterm/addon-webgl@0.19.0-beta.17": - version "0.19.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.17.tgz#68ad9e68dd1cf581b391971de33f5c04966b0d8e" - integrity sha512-X8ObRgoZl7UZTgdndM+mpSO3hLzAhWKoXXrGvUQg/7XabRKAPrQ2XvdyZm04nYwibE6Tpit2h5kkxjlVqupIig== +"@xterm/addon-unicode11@0.9.0-beta.36": + version "0.9.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.36.tgz#158dcdd707a466958a256a960e5d9a967a97a9dc" + integrity sha512-BKP2ml0fYOHnfaTp0LorSluNXjHRSEwf3yrD3K6jEZfYTBePhee1TAxOdNH/TdqwNYZYaYHaK87A5mSuYpKPBQ== -"@xterm/headless@5.6.0-beta.17": - version "5.6.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.6.0-beta.17.tgz#bff1d67c9c061c57adff22571e733d54e3aba2b7" - integrity sha512-ehS7y/XRqX1ppx4RPiYc0vu0SdIQ91aA4lSN/2XNOf3IGdP0A38Q7a0T6mzqxRGZKiiyA0kTR1szr78wnY+wmA== +"@xterm/addon-webgl@0.19.0-beta.36": + version "0.19.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.36.tgz#8926a0434e5ce74eee12a965c06cd5f601391f18" + integrity sha512-bJA1enVNlIMRkBU9i7i8qX26Zs2/CrGedREW5WI0NZUAn0IHlatWlj3aOfTuI2MYWUPGE8ul30PyipYP6P+fmA== -"@xterm/xterm@5.6.0-beta.17": - version "5.6.0-beta.17" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.17.tgz#67ce2e2ff45bd6cc9f26d455d5522c6c4a122ed9" - integrity sha512-+wAv8PhaGQSN9yXWIa8EFtT33pbrA4lZakMB1P05fr+DQ7zoH66QOAUoDY95uOf/4+S6Ihz8wzP2+FH8zETQEA== +"@xterm/headless@5.6.0-beta.36": + version "5.6.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.6.0-beta.36.tgz#cf3e690024019eac2e22d87e0e9f04da6e99cfa9" + integrity sha512-X0Te4ssxcVZ3/YlYEjzN+4w5e4f3Ni/kdjBUKoyZSRpA1+Er54HC/I3t1jc4amqI9xysnVwhq+Ey+LjygIfALw== + +"@xterm/xterm@5.6.0-beta.36": + version "5.6.0-beta.36" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.6.0-beta.36.tgz#fd0fd598b67e3bcba61a59bb1a33b131ad86eea3" + integrity sha512-YtFKQIggbvV2brWifksZAtLi447j0DFdoSRoq4vQi/N7KFC0pguGdG3YzYkDOyqoeLMPu569e2b5oevMe6d2aQ== agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" @@ -156,6 +171,13 @@ agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: dependencies: debug "^4.3.4" +agent-base@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -178,11 +200,11 @@ bl@^4.0.3: readable-stream "^3.4.0" braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" buffer-crc32@~0.2.3: version "0.2.13" @@ -255,10 +277,10 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -267,12 +289,21 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= -graceful-fs@4.2.11: +graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -308,10 +339,13 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -ip@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105" - integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ== +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" is-extglob@^2.1.1: version "2.1.1" @@ -330,10 +364,29 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -jschardet@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" - integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +js-base64@^3.7.5: + version "3.7.7" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" + integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== + +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +jschardet@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.1.3.tgz#10c2289fdae91a0aa9de8bba9c59055fd78898d3" + integrity sha512-Q1PKVMK/uu+yjdlobgWIYkUOCR1SqUmW9m/eUJNNj4zI2N12i25v8fYpVf+zCakQeaTdBdhnZTFbVIAVZIVVOg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" kerberos@^2.0.1: version "2.0.1" @@ -532,22 +585,27 @@ smart-buffer@^4.2.0: integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== socks-proxy-agent@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz#ffc5859a66dac89b0c4dab90253b96705f3e7120" - integrity sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ== + version "8.0.4" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c" + integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== dependencies: - agent-base "^7.0.1" + agent-base "^7.1.1" debug "^4.3.4" - socks "^2.7.1" + socks "^2.8.3" -socks@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" - integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== +socks@^2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== dependencies: - ip "^2.0.0" + ip-address "^9.0.5" smart-buffer "^4.2.0" +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -600,11 +658,21 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + vscode-oniguruma@1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" diff --git a/resources/linux/snap/snapcraft.yaml b/resources/linux/snap/snapcraft.yaml index b7b93f4c59c..1d7412bdc71 100644 --- a/resources/linux/snap/snapcraft.yaml +++ b/resources/linux/snap/snapcraft.yaml @@ -30,15 +30,18 @@ parts: - libcurl3-gnutls - libcurl3-nss - libcurl4 + - libegl1 - libdrm2 - libgbm1 - libgl1 + - libgles2 - libglib2.0-0 - libgtk-3-0 - libibus-1.0-5 - libnss3 - libpango-1.0-0 - libsecret-1-0 + - libwayland-egl1 - libxcomposite1 - libxdamage1 - libxfixes3 diff --git a/resources/server/bin/code-server-linux.sh b/resources/server/bin/code-server-linux.sh index 9229ec89a0e..3df32dfd43c 100644 --- a/resources/server/bin/code-server-linux.sh +++ b/resources/server/bin/code-server-linux.sh @@ -9,8 +9,4 @@ esac ROOT="$(dirname "$(dirname "$(readlink -f "$0")")")" -# workaround for https://github.com/microsoft/vscode/issues/212678 -# Remove this once we update to Node.js >= 20.11.x -export UV_USE_IO_URING=0 - "$ROOT/node" ${INSPECT:-} "$ROOT/out/server-main.js" "$@" diff --git a/scripts/code.bat b/scripts/code.bat index 008c54fcbde..7f48b753559 100644 --- a/scripts/code.bat +++ b/scripts/code.bat @@ -23,9 +23,16 @@ set VSCODE_CLI=1 set ELECTRON_ENABLE_LOGGING=1 set ELECTRON_ENABLE_STACK_DUMPING=1 +set DISABLE_TEST_EXTENSION="--disable-extension=vscode.vscode-api-tests" +for %%A in (%*) do ( + if "%%~A"=="--extensionTestsPath" ( + set DISABLE_TEST_EXTENSION="" + ) +) + :: Launch Code -%CODE% . %* +%CODE% . %DISABLE_TEST_EXTENSION% %* goto end :builtin diff --git a/scripts/code.sh b/scripts/code.sh index 24929fdf351..c29b632cbcb 100755 --- a/scripts/code.sh +++ b/scripts/code.sh @@ -42,8 +42,13 @@ function code() { export ELECTRON_ENABLE_STACK_DUMPING=1 export ELECTRON_ENABLE_LOGGING=1 + DISABLE_TEST_EXTENSION="--disable-extension=vscode.vscode-api-tests" + if [[ "$@" == *"--extensionTestsPath"* ]]; then + DISABLE_TEST_EXTENSION="" + fi + # Launch Code - exec "$CODE" . "$@" + exec "$CODE" . $DISABLE_TEST_EXTENSION "$@" } function code-wsl() diff --git a/scripts/playground-server.ts b/scripts/playground-server.ts index 1c2074ee191..474f83def86 100644 --- a/scripts/playground-server.ts +++ b/scripts/playground-server.ts @@ -223,10 +223,10 @@ class DirWatcher { function handleGetFileChangesRequest(watcher: DirWatcher, fileServer: FileServer, moduleIdMapper: SimpleModuleIdPathMapper): ChainableRequestHandler { return async (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); - const d = watcher.onDidChange(fsPath => { + const d = watcher.onDidChange((fsPath, newContent) => { const path = fileServer.filePathToUrlPath(fsPath); if (path) { - res.write(JSON.stringify({ changedPath: path, moduleId: moduleIdMapper.getModuleId(fsPath) }) + '\n'); + res.write(JSON.stringify({ changedPath: path, moduleId: moduleIdMapper.getModuleId(fsPath), newContent }) + '\n'); } }); res.on('close', () => d.dispose()); @@ -235,13 +235,15 @@ function handleGetFileChangesRequest(watcher: DirWatcher, fileServer: FileServer function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): string { loaderJsCode = loaderJsCode.replace( /constructor\(env, scriptLoader, defineFunc, requireFunc, loaderAvailableTimestamp = 0\) {/, - '$&globalThis.___globalModuleManager = this;' + '$&globalThis.___globalModuleManager = this; globalThis.vscode = { process: { env: { VSCODE_DEV: true } } }' ); const ___globalModuleManager: any = undefined; // This code will be appended to loader.js function $watchChanges(fileChangesUrl: string) { + interface HotReloadConfig { } + let reloadFn; if (globalThis.$sendMessageToParent) { reloadFn = () => globalThis.$sendMessageToParent({ kind: 'reload' }); @@ -262,49 +264,18 @@ function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): s buffer += new TextDecoder().decode(value); const lines = buffer.split('\n'); buffer = lines.pop()!; + + const changes: { relativePath: string; config: HotReloadConfig | undefined; path: string; newContent: string }[] = []; + for (const line of lines) { const data = JSON.parse(line); - let handled = false; - if (data.changedPath.endsWith('.css')) { - if (typeof document !== 'undefined') { - console.log('css changed', data.changedPath); - const styleSheet = [...document.querySelectorAll(`link[rel='stylesheet']`)].find((l: any) => new URL(l.href, document.location.href).pathname.endsWith(data.changedPath)) as any; - if (styleSheet) { - styleSheet.href = styleSheet.href.replace(/\?.*/, '') + '?' + Date.now(); - } - } - handled = true; - } else if (data.changedPath.endsWith('.js') && data.moduleId) { - console.log('js changed', data.changedPath); - const moduleId = ___globalModuleManager._moduleIdProvider.getModuleId(data.moduleId); - if (___globalModuleManager._modules2[moduleId]) { - const srcUrl = ___globalModuleManager._config.moduleIdToPaths(data.moduleId); - const newSrc = await (await fetch(srcUrl)).text(); - (new Function('define', newSrc))(function (deps, callback) { // CodeQL [SM01632] This code is only executed during development (as part of the dev-only playground-server). It is required for the hot-reload functionality. - const oldModule = ___globalModuleManager._modules2[moduleId]; - delete ___globalModuleManager._modules2[moduleId]; + const relativePath = data.changedPath.replace(/\\/g, '/').split('/out/')[1]; + changes.push({ config: {}, path: data.changedPath, relativePath, newContent: data.newContent }); + } - ___globalModuleManager.defineModule(data.moduleId, deps, callback); - const newModule = ___globalModuleManager._modules2[moduleId]; - const oldExports = { ...oldModule.exports }; - - Object.assign(oldModule.exports, newModule.exports); - newModule.exports = oldModule.exports; - - handled = true; - - for (const cb of [...globalThis.$hotReload_deprecateExports]) { - cb(oldExports, newModule.exports); - } - - if (handled) { - console.log('hot reloaded', data.moduleId); - } - }); - } - } - - if (!handled) { reloadFn(); } + const result = handleChanges(changes, 'playground-server'); + if (result.reloadFailedJsFiles.length > 0) { + reloadFn(); } } }).catch(err => { @@ -312,6 +283,163 @@ function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): s setTimeout(() => $watchChanges(fileChangesUrl), 1000); }); + + function handleChanges(changes: { + relativePath: string; + config: HotReloadConfig | undefined; + path: string; + newContent: string; + }[], debugSessionName: string) { + // This function is stringified and injected into the debuggee. + + const hotReloadData: { count: number; originalWindowTitle: any; timeout: any; shouldReload: boolean } = globalThis.$hotReloadData || (globalThis.$hotReloadData = { count: 0, messageHideTimeout: undefined, shouldReload: false }); + + const reloadFailedJsFiles: { relativePath: string; path: string }[] = []; + + for (const change of changes) { + handleChange(change.relativePath, change.path, change.newContent, change.config); + } + + return { reloadFailedJsFiles }; + + function handleChange(relativePath: string, path: string, newSrc: string, config: any) { + if (relativePath.endsWith('.css')) { + handleCssChange(relativePath); + } else if (relativePath.endsWith('.js')) { + handleJsChange(relativePath, path, newSrc, config); + } + } + + function handleCssChange(relativePath: string) { + if (typeof document === 'undefined') { + return; + } + + const styleSheet = (([...document.querySelectorAll(`link[rel='stylesheet']`)] as HTMLLinkElement[])) + .find(l => new URL(l.href, document.location.href).pathname.endsWith(relativePath)); + if (styleSheet) { + setMessage(`reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); + console.log(debugSessionName, 'css reloaded', relativePath); + styleSheet.href = styleSheet.href.replace(/\?.*/, '') + '?' + Date.now(); + } else { + setMessage(`could not reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); + console.log(debugSessionName, 'ignoring css change, as stylesheet is not loaded', relativePath); + } + } + + + function handleJsChange(relativePath: string, path: string, newSrc: string, config: any) { + const moduleIdStr = trimEnd(relativePath, '.js'); + + const requireFn: any = globalThis.require; + const moduleManager = (requireFn as any).moduleManager; + if (!moduleManager) { + console.log(debugSessionName, 'ignoring js change, as moduleManager is not available', relativePath); + return; + } + + const moduleId = moduleManager._moduleIdProvider.getModuleId(moduleIdStr); + const oldModule = moduleManager._modules2[moduleId]; + + if (!oldModule) { + console.log(debugSessionName, 'ignoring js change, as module is not loaded', relativePath); + return; + } + + // Check if we can reload + const g = globalThis as any; + + // A frozen copy of the previous exports + const oldExports = Object.freeze({ ...oldModule.exports }); + const reloadFn = g.$hotReload_applyNewExports?.({ oldExports, newSrc, config }); + + if (!reloadFn) { + console.log(debugSessionName, 'ignoring js change, as module does not support hot-reload', relativePath); + hotReloadData.shouldReload = true; + + reloadFailedJsFiles.push({ relativePath, path }); + + setMessage(`hot reload not supported for ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); + return; + } + + // Eval maintains source maps + function newScript(/* this parameter is used by newSrc */ define) { + // eslint-disable-next-line no-eval + eval(newSrc); // CodeQL [SM01632] This code is only executed during development. It is required for the hot-reload functionality. + } + + newScript(/* define */ function (deps, callback) { + // Evaluating the new code was successful. + + // Redefine the module + delete moduleManager._modules2[moduleId]; + moduleManager.defineModule(moduleIdStr, deps, callback); + const newModule = moduleManager._modules2[moduleId]; + + + // Patch the exports of the old module, so that modules using the old module get the new exports + Object.assign(oldModule.exports, newModule.exports); + // We override the exports so that future reloads still patch the initial exports. + newModule.exports = oldModule.exports; + + const successful = reloadFn(newModule.exports); + if (!successful) { + hotReloadData.shouldReload = true; + setMessage(`hot reload failed ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); + console.log(debugSessionName, 'hot reload was not successful', relativePath); + return; + } + + console.log(debugSessionName, 'hot reloaded', moduleIdStr); + setMessage(`successfully reloaded ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); + }); + } + + function setMessage(message: string) { + const domElem = (document.querySelector('.titlebar-center .window-title')) as HTMLDivElement | undefined; + if (!domElem) { return; } + if (!hotReloadData.timeout) { + hotReloadData.originalWindowTitle = domElem.innerText; + } else { + clearTimeout(hotReloadData.timeout); + } + if (hotReloadData.shouldReload) { + message += ' (manual reload required)'; + } + + domElem.innerText = message; + hotReloadData.timeout = setTimeout(() => { + hotReloadData.timeout = undefined; + // If wanted, we can restore the previous title message + // domElem.replaceChildren(hotReloadData.originalWindowTitle); + }, 5000); + } + + function formatPath(path: string): string { + const parts = path.split('/'); + parts.reverse(); + let result = parts[0]; + parts.shift(); + for (const p of parts) { + if (result.length + p.length > 40) { + break; + } + result = p + '/' + result; + if (result.length > 20) { + break; + } + } + return result; + } + + function trimEnd(str, suffix) { + if (str.endsWith(suffix)) { + return str.substring(0, str.length - suffix.length); + } + return str; + } + } } const additionalJsCode = ` diff --git a/scripts/xterm-update.js b/scripts/xterm-update.js index 851b296af62..8ede619160b 100644 --- a/scripts/xterm-update.js +++ b/scripts/xterm-update.js @@ -8,6 +8,7 @@ const path = require('path'); const moduleNames = [ '@xterm/xterm', + '@xterm/addon-clipboard', '@xterm/addon-image', '@xterm/addon-search', '@xterm/addon-serialize', diff --git a/src/bootstrap-amd.js b/src/bootstrap-amd.js index cc47b050fb5..27d15eb76d5 100644 --- a/src/bootstrap-amd.js +++ b/src/bootstrap-amd.js @@ -6,6 +6,11 @@ //@ts-check 'use strict'; +/** + * @typedef {import('./vs/nls').INLSConfiguration} INLSConfiguration + * @import { IProductConfiguration } from './vs/base/common/product' + */ + // Store the node.js require function in a variable // before loading our AMD loader to avoid issues // when this file is bundled with other files. @@ -15,8 +20,8 @@ const nodeRequire = require; globalThis._VSCODE_NODE_MODULES = new Proxy(Object.create(null), { get: (_target, mod) => nodeRequire(String(mod)) }); // VSCODE_GLOBALS: package/product.json -/** @type Record */ -globalThis._VSCODE_PRODUCT_JSON = require('../product.json'); +/** @type Partial */ +globalThis._VSCODE_PRODUCT_JSON = require('./bootstrap-meta').product; if (process.env['VSCODE_DEV']) { // Patch product overrides when running out of sources try { @@ -25,22 +30,19 @@ if (process.env['VSCODE_DEV']) { globalThis._VSCODE_PRODUCT_JSON = Object.assign(globalThis._VSCODE_PRODUCT_JSON, overrides); } catch (error) { /* ignore */ } } -globalThis._VSCODE_PACKAGE_JSON = require('../package.json'); +globalThis._VSCODE_PACKAGE_JSON = require('./bootstrap-meta').pkg; // @ts-ignore const loader = require('./vs/loader'); const bootstrap = require('./bootstrap'); const performance = require('./vs/base/common/performance'); - -// Bootstrap: NLS -const nlsConfig = bootstrap.setupNLS(); +const fs = require('fs'); // Bootstrap: Loader loader.config({ baseUrl: bootstrap.fileUriFromPath(__dirname, { isWindows: process.platform === 'win32' }), catchError: true, nodeRequire, - 'vs/nls': nlsConfig, amdModulesPattern: /^vs\//, recordStats: true }); @@ -52,13 +54,90 @@ if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) { }); } -// Pseudo NLS support -if (nlsConfig && nlsConfig.pseudo) { - loader(['vs/nls'], function (/** @type {import('vs/nls')} */nlsPlugin) { - nlsPlugin.setPseudoTranslation(!!nlsConfig.pseudo); - }); +//#region NLS helpers + +/** @type {Promise | undefined} */ +let setupNLSResult = undefined; + +/** + * @returns {Promise} + */ +function setupNLS() { + if (!setupNLSResult) { + setupNLSResult = doSetupNLS(); + } + + return setupNLSResult; } +/** + * @returns {Promise} + */ +async function doSetupNLS() { + performance.mark('code/amd/willLoadNls'); + + /** @type {INLSConfiguration | undefined} */ + let nlsConfig = undefined; + + /** @type {string | undefined} */ + let messagesFile; + if (process.env['VSCODE_NLS_CONFIG']) { + try { + /** @type {INLSConfiguration} */ + nlsConfig = JSON.parse(process.env['VSCODE_NLS_CONFIG']); + if (nlsConfig?.languagePack?.messagesFile) { + messagesFile = nlsConfig.languagePack.messagesFile; + } else if (nlsConfig?.defaultMessagesFile) { + messagesFile = nlsConfig.defaultMessagesFile; + } + + // VSCODE_GLOBALS: NLS + globalThis._VSCODE_NLS_LANGUAGE = nlsConfig?.resolvedLanguage; + } catch (e) { + console.error(`Error reading VSCODE_NLS_CONFIG from environment: ${e}`); + } + } + + if ( + process.env['VSCODE_DEV'] || // no NLS support in dev mode + !messagesFile // no NLS messages file + ) { + return undefined; + } + + try { + // VSCODE_GLOBALS: NLS + globalThis._VSCODE_NLS_MESSAGES = JSON.parse((await fs.promises.readFile(messagesFile)).toString()); + } catch (error) { + console.error(`Error reading NLS messages file ${messagesFile}: ${error}`); + + // Mark as corrupt: this will re-create the language pack cache next startup + if (nlsConfig?.languagePack?.corruptMarkerFile) { + try { + await fs.promises.writeFile(nlsConfig.languagePack.corruptMarkerFile, 'corrupted'); + } catch (error) { + console.error(`Error writing corrupted NLS marker file: ${error}`); + } + } + + // Fallback to the default message file to ensure english translation at least + if (nlsConfig?.defaultMessagesFile && nlsConfig.defaultMessagesFile !== messagesFile) { + try { + // VSCODE_GLOBALS: NLS + globalThis._VSCODE_NLS_MESSAGES = JSON.parse((await fs.promises.readFile(nlsConfig.defaultMessagesFile)).toString()); + } catch (error) { + console.error(`Error reading default NLS messages file ${nlsConfig.defaultMessagesFile}: ${error}`); + } + } + } + + performance.mark('code/amd/didLoadNls'); + + return nlsConfig; +} + +//#endregion + /** * @param {string=} entrypoint * @param {(value: any) => void=} onLoad @@ -82,6 +161,8 @@ exports.load = function (entrypoint, onLoad, onError) { onLoad = onLoad || function () { }; onError = onError || function (err) { console.error(err); }; - performance.mark('code/fork/willLoadCode'); - loader([entrypoint], onLoad, onError); + setupNLS().then(() => { + performance.mark('code/fork/willLoadCode'); + loader([entrypoint], onLoad, onError); + }); }; diff --git a/src/bootstrap-meta.js b/src/bootstrap-meta.js new file mode 100644 index 00000000000..7924b77eec8 --- /dev/null +++ b/src/bootstrap-meta.js @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check +'use strict'; + +/** + * @import { IProductConfiguration } from './vs/base/common/product' + */ + +/** @type Partial & { BUILD_INSERT_PRODUCT_CONFIGURATION?: string } */ +let product = { BUILD_INSERT_PRODUCT_CONFIGURATION: 'BUILD_INSERT_PRODUCT_CONFIGURATION' }; // DO NOT MODIFY, PATCHED DURING BUILD +if (product['BUILD_INSERT_PRODUCT_CONFIGURATION']) { + // @ts-ignore + product = require('../product.json'); // Running out of sources +} + +/** @type object & { BUILD_INSERT_PACKAGE_CONFIGURATION?: string } */ +let pkg = { BUILD_INSERT_PACKAGE_CONFIGURATION: 'BUILD_INSERT_PACKAGE_CONFIGURATION' }; // DO NOT MODIFY, PATCHED DURING BUILD +if (pkg['BUILD_INSERT_PACKAGE_CONFIGURATION']) { + // @ts-ignore + pkg = require('../package.json'); // Running out of sources +} + +exports.product = product; +exports.pkg = pkg; diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index fa3bc5eb839..cd859a847f9 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -8,6 +8,10 @@ //@ts-check 'use strict'; +/** + * @import { ISandboxConfiguration } from './vs/base/parts/sandbox/common/sandboxTypes' + */ + /* eslint-disable no-restricted-globals */ // Simple module style to support node.js and browser environments @@ -29,8 +33,6 @@ const safeProcess = preloadGlobals.process; /** - * @typedef {import('./vs/base/parts/sandbox/common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - * * @param {string[]} modulePaths * @param {(result: unknown, configuration: ISandboxConfiguration) => Promise | undefined} resultCallback * @param {{ @@ -80,28 +82,23 @@ developerDeveloperKeybindingsDisposable = registerDeveloperKeybindings(disallowReloadKeybinding); } - // Get the nls configuration into the process.env as early as possible - // @ts-ignore - const nlsConfig = globalThis.MonacoBootstrap.setupNLS(); - - let locale = nlsConfig.availableLanguages['*'] || 'en'; - if (locale === 'zh-tw') { - locale = 'zh-Hant'; - } else if (locale === 'zh-cn') { - locale = 'zh-Hans'; + // VSCODE_GLOBALS: NLS + globalThis._VSCODE_NLS_MESSAGES = configuration.nls.messages; + globalThis._VSCODE_NLS_LANGUAGE = configuration.nls.language; + let language = configuration.nls.language || 'en'; + if (language === 'zh-tw') { + language = 'zh-Hant'; + } else if (language === 'zh-cn') { + language = 'zh-Hans'; } - window.document.documentElement.setAttribute('lang', locale); + window.document.documentElement.setAttribute('lang', language); window['MonacoEnvironment'] = {}; - /** - * @typedef {any} LoaderConfig - */ - /** @type {LoaderConfig} */ + /** @type {any} */ const loaderConfig = { baseUrl: `${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32', scheme: 'vscode-file', fallbackAuthority: 'vscode-app' })}/out`, - 'vs/nls': nlsConfig, preferScriptTags: true }; @@ -124,6 +121,7 @@ 'vscode-oniguruma': `${baseNodeModulesPath}/vscode-oniguruma/release/main.js`, 'vsda': `${baseNodeModulesPath}/vsda/index.js`, '@xterm/xterm': `${baseNodeModulesPath}/@xterm/xterm/lib/xterm.js`, + '@xterm/addon-clipboard': `${baseNodeModulesPath}/@xterm/addon-clipboard/lib/addon-clipboard.js`, '@xterm/addon-image': `${baseNodeModulesPath}/@xterm/addon-image/lib/addon-image.js`, '@xterm/addon-search': `${baseNodeModulesPath}/@xterm/addon-search/lib/addon-search.js`, '@xterm/addon-serialize': `${baseNodeModulesPath}/@xterm/addon-serialize/lib/addon-serialize.js`, @@ -144,13 +142,6 @@ // Configure loader require.config(loaderConfig); - // Handle pseudo NLS - if (nlsConfig.pseudo) { - require(['vs/nls'], function (nlsPlugin) { - nlsPlugin.setPseudoTranslation(nlsConfig.pseudo); - }); - } - // Signal before require() if (typeof options?.beforeRequire === 'function') { options.beforeRequire(configuration); diff --git a/src/bootstrap.js b/src/bootstrap.js index bd0e92e672c..8be098f4199 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -22,8 +22,6 @@ }(this, function () { const Module = typeof require === 'function' ? require('module') : undefined; const path = typeof require === 'function' ? require('path') : undefined; - const fs = typeof require === 'function' ? require('fs') : undefined; - const util = typeof require === 'function' ? require('util') : undefined; //#region global bootstrapping @@ -118,141 +116,8 @@ //#endregion - - //#region NLS helpers - - /** - * @returns {{locale?: string, availableLanguages: {[lang: string]: string;}, pseudo?: boolean } | undefined} - */ - function setupNLS() { - - // Get the nls configuration as early as possible. - const process = safeProcess(); - /** @type {{ availableLanguages: {}; loadBundle?: (bundle: string, language: string, cb: (err: Error | undefined, result: string | undefined) => void) => void; _resolvedLanguagePackCoreLocation?: string; _corruptedFile?: string }} */ - let nlsConfig = { availableLanguages: {} }; - if (process && process.env['VSCODE_NLS_CONFIG']) { - try { - nlsConfig = JSON.parse(process.env['VSCODE_NLS_CONFIG']); - } catch (e) { - // Ignore - } - } - - if (nlsConfig._resolvedLanguagePackCoreLocation) { - const bundles = Object.create(null); - - /** - * @param {string} bundle - * @param {string} language - * @param {(err: Error | undefined, result: string | undefined) => void} cb - */ - nlsConfig.loadBundle = function (bundle, language, cb) { - const result = bundles[bundle]; - if (result) { - cb(undefined, result); - - return; - } - - // @ts-ignore - safeReadNlsFile(nlsConfig._resolvedLanguagePackCoreLocation, `${bundle.replace(/\//g, '!')}.nls.json`).then(function (content) { - const json = JSON.parse(content); - bundles[bundle] = json; - - cb(undefined, json); - }).catch((error) => { - try { - if (nlsConfig._corruptedFile) { - safeWriteNlsFile(nlsConfig._corruptedFile, 'corrupted').catch(function (error) { console.error(error); }); - } - } finally { - cb(error, undefined); - } - }); - }; - } - - return nlsConfig; - } - - /** - * @returns {typeof import('./vs/base/parts/sandbox/electron-sandbox/globals') | undefined} - */ - function safeSandboxGlobals() { - const globals = (typeof self === 'object' ? self : typeof global === 'object' ? global : {}); - - // @ts-ignore - return globals.vscode; - } - - /** - * @returns {import('./vs/base/parts/sandbox/electron-sandbox/globals').ISandboxNodeProcess | NodeJS.Process | undefined} - */ - function safeProcess() { - const sandboxGlobals = safeSandboxGlobals(); - if (sandboxGlobals) { - return sandboxGlobals.process; // Native environment (sandboxed) - } - - if (typeof process !== 'undefined') { - return process; // Native environment (non-sandboxed) - } - - return undefined; - } - - /** - * @returns {import('./vs/base/parts/sandbox/electron-sandbox/electronTypes').IpcRenderer | undefined} - */ - function safeIpcRenderer() { - const sandboxGlobals = safeSandboxGlobals(); - if (sandboxGlobals) { - return sandboxGlobals.ipcRenderer; - } - - return undefined; - } - - /** - * @param {string[]} pathSegments - * @returns {Promise} - */ - async function safeReadNlsFile(...pathSegments) { - const ipcRenderer = safeIpcRenderer(); - if (ipcRenderer) { - return ipcRenderer.invoke('vscode:readNlsFile', ...pathSegments); - } - - if (fs && path && util) { - return (await util.promisify(fs.readFile)(path.join(...pathSegments))).toString(); - } - - throw new Error('Unsupported operation (read NLS files)'); - } - - /** - * @param {string} path - * @param {string} content - * @returns {Promise} - */ - function safeWriteNlsFile(path, content) { - const ipcRenderer = safeIpcRenderer(); - if (ipcRenderer) { - return ipcRenderer.invoke('vscode:writeNlsFile', path, content); - } - - if (fs && util) { - return util.promisify(fs.writeFile)(path, content); - } - - throw new Error('Unsupported operation (write NLS files)'); - } - - //#endregion - return { enableASARSupport, - setupNLS, fileUriFromPath }; })); diff --git a/src/buildfile.js b/src/buildfile.js index f03de33fb3d..cfe4b389e55 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -9,7 +9,7 @@ */ function createModuleDescription(name, exclude) { - let excludes = ['vs/css', 'vs/nls']; + let excludes = ['vs/css']; if (Array.isArray(exclude) && exclude.length > 0) { excludes = excludes.concat(exclude); } @@ -32,7 +32,7 @@ exports.base = [ { name: 'vs/editor/common/services/editorSimpleWorker', include: ['vs/base/common/worker/simpleWorker'], - exclude: ['vs/nls'], + exclude: [], prepend: [ { path: 'vs/loader.js' }, { path: 'vs/base/worker/workerMain.js' } @@ -41,7 +41,7 @@ exports.base = [ }, { name: 'vs/base/common/worker/simpleWorker', - exclude: ['vs/nls'], + exclude: [], } ]; @@ -57,7 +57,8 @@ exports.workbenchDesktop = [ createModuleDescription('vs/workbench/contrib/debug/node/telemetryApp'), createModuleDescription('vs/platform/files/node/watcher/watcherMain'), createModuleDescription('vs/platform/terminal/node/ptyHostMain'), - createModuleDescription('vs/workbench/api/node/extensionHostProcess') + createModuleDescription('vs/workbench/api/node/extensionHostProcess'), + createModuleDescription('vs/workbench/contrib/issue/electron-sandbox/issueReporterMain'), ]; exports.workbenchWeb = [ @@ -76,7 +77,6 @@ exports.code = [ createModuleDescription('vs/code/electron-main/main'), createModuleDescription('vs/code/node/cli'), createModuleDescription('vs/code/node/cliProcessMain', ['vs/code/node/cli']), - createModuleDescription('vs/code/electron-sandbox/issue/issueReporterMain'), createModuleDescription('vs/code/node/sharedProcess/sharedProcessMain'), createModuleDescription('vs/code/electron-sandbox/processExplorer/processExplorerMain') ]; diff --git a/src/cli.js b/src/cli.js index a8aaa1f0d54..8191425eff9 100644 --- a/src/cli.js +++ b/src/cli.js @@ -16,17 +16,27 @@ delete process.env['VSCODE_CWD']; const bootstrap = require('./bootstrap'); const bootstrapNode = require('./bootstrap-node'); -const product = require('../product.json'); +const product = require('./bootstrap-meta').product; +const { resolveNLSConfiguration } = require('./vs/base/node/nls'); -// Enable portable support -// @ts-ignore -bootstrapNode.configurePortable(product); +async function start() { -// Enable ASAR support -bootstrap.enableASARSupport(); + // NLS + const nlsConfiguration = await resolveNLSConfiguration({ userLocale: 'en', osLocale: 'en', commit: product.commit, userDataPath: '', nlsMetadataPath: __dirname }); + process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfiguration); // required for `bootstrap-amd` to pick up NLS messages -// Signal processes that we got launched as CLI -process.env['VSCODE_CLI'] = '1'; + // Enable portable support + // @ts-ignore + bootstrapNode.configurePortable(product); -// Load CLI through AMD loader -require('./bootstrap-amd').load('vs/code/node/cli'); + // Enable ASAR support + bootstrap.enableASARSupport(); + + // Signal processes that we got launched as CLI + process.env['VSCODE_CLI'] = '1'; + + // Load CLI through AMD loader + require('./bootstrap-amd').load('vs/code/node/cli'); +} + +start(); diff --git a/src/main.js b/src/main.js index 9fe5654081d..be7cd98afaf 100644 --- a/src/main.js +++ b/src/main.js @@ -7,25 +7,22 @@ 'use strict'; /** - * @typedef {import('./vs/base/common/product').IProductConfiguration} IProductConfiguration - * @typedef {import('./vs/base/node/languagePacks').NLSConfiguration} NLSConfiguration - * @typedef {import('./vs/platform/environment/common/argv').NativeParsedArgs} NativeParsedArgs + * @import { INLSConfiguration } from './vs/nls' + * @import { NativeParsedArgs } from './vs/platform/environment/common/argv' */ const perf = require('./vs/base/common/performance'); perf.mark('code/didStartMain'); const path = require('path'); -const fs = require('fs'); +const fs = require('original-fs'); const os = require('os'); const bootstrap = require('./bootstrap'); const bootstrapNode = require('./bootstrap-node'); const { getUserDataPath } = require('./vs/platform/environment/node/userDataPath'); -const { stripComments } = require('./vs/base/common/stripComments'); +const { parse } = require('./vs/base/common/jsonc'); const { getUNCHost, addUNCHostToAllowlist } = require('./vs/base/node/unc'); -/** @type {Partial} */ -// @ts-ignore -const product = require('../product.json'); +const product = require('./bootstrap-meta').product; const { app, protocol, crashReporter, Menu } = require('electron'); // Enable portable support @@ -109,27 +106,29 @@ protocol.registerSchemesAsPrivileged([ registerListeners(); /** - * Support user defined locale: load it early before app('ready') - * to have more things running in parallel. + * We can resolve the NLS configuration early if it is defined + * in argv.json before `app.ready` event. Otherwise we can only + * resolve NLS after `app.ready` event to resolve the OS locale. * - * @type {Promise | undefined} + * @type {Promise | undefined} */ let nlsConfigurationPromise = undefined; -/** - * @type {String} - **/ // Use the most preferred OS language for language recommendation. // The API might return an empty array on Linux, such as when // the 'C' locale is the user's only configured locale. // No matter the OS, if the array is empty, default back to 'en'. -const resolved = app.getPreferredSystemLanguages()?.[0] ?? 'en'; -const osLocale = processZhLocale(resolved.toLowerCase()); -const metaDataFile = path.join(__dirname, 'nls.metadata.json'); -const locale = getUserDefinedLocale(argvConfig); -if (locale) { - const { getNLSConfiguration } = require('./vs/base/node/languagePacks'); - nlsConfigurationPromise = getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale, osLocale); +const osLocale = processZhLocale((app.getPreferredSystemLanguages()?.[0] ?? 'en').toLowerCase()); +const userLocale = getUserDefinedLocale(argvConfig); +if (userLocale) { + const { resolveNLSConfiguration } = require('./vs/base/node/nls'); + nlsConfigurationPromise = resolveNLSConfiguration({ + userLocale, + osLocale, + commit: product.commit, + userDataPath, + nlsMetadataPath: __dirname + }); } // Pass in the locale to Electron so that the @@ -141,7 +140,7 @@ if (locale) { // In that case, use `en` as the Electron locale. if (process.platform === 'win32' || process.platform === 'linux') { - const electronLocale = (!locale || locale === 'qps-ploc') ? 'en' : locale; + const electronLocale = (!userLocale || userLocale === 'qps-ploc') ? 'en' : userLocale; app.commandLine.appendSwitch('lang', electronLocale); } @@ -161,15 +160,28 @@ app.once('ready', function () { } }); +async function onReady() { + perf.mark('code/mainAppReady'); + + try { + const [, nlsConfig] = await Promise.all([ + mkdirpIgnoreError(codeCachePath), + resolveNlsConfiguration() + ]); + + startup(codeCachePath, nlsConfig); + } catch (error) { + console.error(error); + } +} + /** * Main startup routine * * @param {string | undefined} codeCachePath - * @param {NLSConfiguration} nlsConfig + * @param {INLSConfiguration} nlsConfig */ function startup(codeCachePath, nlsConfig) { - nlsConfig._languagePackSupport = true; - process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig); process.env['VSCODE_CODE_CACHE_PATH'] = codeCachePath || ''; @@ -180,18 +192,6 @@ function startup(codeCachePath, nlsConfig) { }); } -async function onReady() { - perf.mark('code/mainAppReady'); - - try { - const [, nlsConfig] = await Promise.all([mkdirpIgnoreError(codeCachePath), resolveNlsConfiguration()]); - - startup(codeCachePath, nlsConfig); - } catch (error) { - console.error(error); - } -} - /** * @param {NativeParsedArgs} cliArgs */ @@ -205,10 +205,14 @@ function configureCommandlineSwitchesSync(cliArgs) { 'force-color-profile', // disable LCD font rendering, a Chromium flag - 'disable-lcd-text' + 'disable-lcd-text', + + // bypass any specified proxy for the given semi-colon-separated list of hosts + 'proxy-bypass-list' ]; if (process.platform === 'linux') { + // Force enable screen readers on Linux via this flag SUPPORTED_ELECTRON_SWITCHES.push('force-renderer-accessibility'); @@ -243,10 +247,7 @@ function configureCommandlineSwitchesSync(cliArgs) { app.commandLine.appendSwitch(argvKey); } } else if (argvValue) { - if (argvKey === 'force-color-profile') { - // Color profile - app.commandLine.appendSwitch(argvKey, argvValue); - } else if (argvKey === 'password-store') { + if (argvKey === 'password-store') { // Password store // TODO@TylerLeonhardt: Remove this migration in 3 months let migratedArgvValue = argvValue; @@ -254,6 +255,8 @@ function configureCommandlineSwitchesSync(cliArgs) { migratedArgvValue = 'gnome-libsecret'; } app.commandLine.appendSwitch(argvKey, migratedArgvValue); + } else { + app.commandLine.appendSwitch(argvKey, argvValue); } } } @@ -294,6 +297,13 @@ function configureCommandlineSwitchesSync(cliArgs) { `CalculateNativeWinOcclusion,${app.commandLine.getSwitchValue('disable-features')}`; app.commandLine.appendSwitch('disable-features', featuresToDisable); + // Blink features to configure. + // `FontMatchingCTMigration` - Siwtch font matching on macOS to CoreText (Refs https://github.com/microsoft/vscode/issues/214390). + // TODO(deepak1556): Enable this feature again after updating to Electron 30. + const blinkFeaturesToDisable = + `FontMatchingCTMigration,${app.commandLine.getSwitchValue('disable-blink-features')}`; + app.commandLine.appendSwitch('disable-blink-features', blinkFeaturesToDisable); + // Support JS Flags const jsFlags = getJSFlags(cliArgs); if (jsFlags) { @@ -309,7 +319,7 @@ function readArgvConfigSync() { const argvConfigPath = getArgvConfigPath(); let argvConfig; try { - argvConfig = JSON.parse(stripComments(fs.readFileSync(argvConfigPath).toString())); + argvConfig = parse(fs.readFileSync(argvConfigPath).toString()); } catch (error) { if (error && error.code === 'ENOENT') { createDefaultArgvConfigSync(argvConfigPath); @@ -577,16 +587,6 @@ function getCodeCachePath() { return path.join(userDataPath, 'CachedData', commit); } -/** - * @param {string} dir - * @returns {Promise} - */ -function mkdirp(dir) { - return new Promise((resolve, reject) => { - fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? reject(err) : resolve(dir)); - }); -} - /** * @param {string | undefined} dir * @returns {Promise} @@ -594,7 +594,7 @@ function mkdirp(dir) { async function mkdirpIgnoreError(dir) { if (typeof dir === 'string') { try { - await mkdirp(dir); + await fs.promises.mkdir(dir, { recursive: true }); return dir; } catch (error) { @@ -634,35 +634,47 @@ function processZhLocale(appLocale) { /** * Resolve the NLS configuration * - * @return {Promise} + * @return {Promise} */ async function resolveNlsConfiguration() { - // First, we need to test a user defined locale. If it fails we try the app locale. + // First, we need to test a user defined locale. + // If it fails we try the app locale. // If that fails we fall back to English. - let nlsConfiguration = nlsConfigurationPromise ? await nlsConfigurationPromise : undefined; + + const nlsConfiguration = nlsConfigurationPromise ? await nlsConfigurationPromise : undefined; if (nlsConfiguration) { return nlsConfiguration; } - // Try to use the app locale. Please note that the app locale is only - // valid after we have received the app ready event. This is why the - // code is here. + // Try to use the app locale which is only valid + // after the app ready event has been fired. - /** - * @type string - */ - let appLocale = app.getLocale(); - if (!appLocale) { - return { locale: 'en', osLocale, availableLanguages: {} }; + let userLocale = app.getLocale(); + if (!userLocale) { + return { + userLocale: 'en', + osLocale, + resolvedLanguage: 'en', + defaultMessagesFile: path.join(__dirname, 'nls.messages.json'), + + // NLS: below 2 are a relic from old times only used by vscode-nls and deprecated + locale: 'en', + availableLanguages: {} + }; } // See above the comment about the loader and case sensitiveness - appLocale = processZhLocale(appLocale.toLowerCase()); + userLocale = processZhLocale(userLocale.toLowerCase()); - const { getNLSConfiguration } = require('./vs/base/node/languagePacks'); - nlsConfiguration = await getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale, osLocale); - return nlsConfiguration ?? { locale: 'en', osLocale, availableLanguages: {} }; + const { resolveNLSConfiguration } = require('./vs/base/node/nls'); + return resolveNLSConfiguration({ + userLocale, + osLocale, + commit: product.commit, + userDataPath, + nlsMetadataPath: __dirname + }); } /** @@ -680,7 +692,7 @@ function getUserDefinedLocale(argvConfig) { return locale.toLowerCase(); // a directly provided --locale always wins } - return argvConfig.locale && typeof argvConfig.locale === 'string' ? argvConfig.locale.toLowerCase() : undefined; + return typeof argvConfig?.locale === 'string' ? argvConfig.locale.toLowerCase() : undefined; } //#endregion diff --git a/src/server-cli.js b/src/server-cli.js index fdfdac48b99..aa041a66251 100644 --- a/src/server-cli.js +++ b/src/server-cli.js @@ -4,18 +4,30 @@ *--------------------------------------------------------------------------------------------*/ // @ts-check +'use strict'; const path = require('path'); +const product = require('./bootstrap-meta').product; +const { resolveNLSConfiguration } = require('./vs/base/node/nls'); -// Keep bootstrap-amd.js from redefining 'fs'. -delete process.env['ELECTRON_RUN_AS_NODE']; +async function start() { -if (process.env['VSCODE_DEV']) { - // When running out of sources, we need to load node modules from remote/node_modules, - // which are compiled against nodejs, not electron - process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] = process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] || path.join(__dirname, '..', 'remote', 'node_modules'); - require('./bootstrap-node').injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']); -} else { - delete process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']; + // Keep bootstrap-amd.js from redefining 'fs'. + delete process.env['ELECTRON_RUN_AS_NODE']; + + // NLS + const nlsConfiguration = await resolveNLSConfiguration({ userLocale: 'en', osLocale: 'en', commit: product.commit, userDataPath: '', nlsMetadataPath: __dirname }); + process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfiguration); // required for `bootstrap-amd` to pick up NLS messages + + if (process.env['VSCODE_DEV']) { + // When running out of sources, we need to load node modules from remote/node_modules, + // which are compiled against nodejs, not electron + process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] = process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] || path.join(__dirname, '..', 'remote', 'node_modules'); + require('./bootstrap-node').injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']); + } else { + delete process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']; + } + require('./bootstrap-amd').load('vs/server/node/server.cli'); } -require('./bootstrap-amd').load('vs/server/node/server.cli'); + +start(); diff --git a/src/server-main.js b/src/server-main.js index 81e88e118f7..e5a90596962 100644 --- a/src/server-main.js +++ b/src/server-main.js @@ -4,12 +4,22 @@ *--------------------------------------------------------------------------------------------*/ // @ts-check +'use strict'; + +/** + * @import { INLSConfiguration } from './vs/nls' + */ + +/** + * @import { IServerAPI } from './vs/server/node/remoteExtensionHostAgentServer' + */ const perf = require('./vs/base/common/performance'); const performance = require('perf_hooks').performance; -const product = require('../product.json'); +const product = require('./bootstrap-meta').product; const readline = require('readline'); const http = require('http'); +const { resolveNLSConfiguration } = require('./vs/base/node/nls'); perf.mark('code/server/start'); // @ts-ignore @@ -38,16 +48,15 @@ async function start() { const shouldSpawnCli = parsedArgs.help || parsedArgs.version || extensionLookupArgs.some(a => !!parsedArgs[a]) || (extensionInstallArgs.some(a => !!parsedArgs[a]) && !parsedArgs['start-server']); + const nlsConfiguration = await resolveNLSConfiguration({ userLocale: 'en', osLocale: 'en', commit: product.commit, userDataPath: '', nlsMetadataPath: __dirname }); + if (shouldSpawnCli) { - loadCode().then((mod) => { + loadCode(nlsConfiguration).then((mod) => { mod.spawnCli(); }); return; } - /** - * @typedef { import('./vs/server/node/remoteExtensionHostAgentServer').IServerAPI } IServerAPI - */ /** @type {IServerAPI | null} */ let _remoteExtensionHostAgentServer = null; /** @type {Promise | null} */ @@ -55,7 +64,7 @@ async function start() { /** @returns {Promise} */ const getRemoteExtensionHostAgentServer = () => { if (!_remoteExtensionHostAgentServerPromise) { - _remoteExtensionHostAgentServerPromise = loadCode().then(async (mod) => { + _remoteExtensionHostAgentServerPromise = loadCode(nlsConfiguration).then(async (mod) => { const server = await mod.createServer(address); _remoteExtensionHostAgentServer = server; return server; @@ -248,13 +257,19 @@ async function findFreePort(host, start, end) { return undefined; } -/** @returns { Promise } */ -function loadCode() { +/** + * @param {INLSConfiguration} nlsConfiguration + * @returns { Promise } + */ +function loadCode(nlsConfiguration) { return new Promise((resolve, reject) => { const path = require('path'); delete process.env['ELECTRON_RUN_AS_NODE']; // Keep bootstrap-amd.js from redefining 'fs'. + /** @type {INLSConfiguration} */ + process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfiguration); // required for `bootstrap-amd` to pick up NLS messages + // See https://github.com/microsoft/vscode-remote-release/issues/6543 // We would normally install a SIGPIPE listener in bootstrap.js // But in certain situations, the console itself can be in a broken pipe state @@ -307,5 +322,4 @@ function prompt(question) { }); } - start(); diff --git a/src/tsconfig.json b/src/tsconfig.json index db4e0e67fa2..b6bc4752d7a 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { + "esModuleInterop": true, "removeComments": false, "preserveConstEnums": true, "sourceMap": false, @@ -34,6 +35,16 @@ "./main.js", "./server-main.js", "./server-cli.js", + "./vs/base/common/jsonc.js", + "./vs/base/common/performance.js", + "./vs/base/node/unc.js", + "./vs/base/node/nls.js", + "./vs/platform/environment/node/userDataPath.js", + "./vs/base/parts/sandbox/electron-sandbox/preload-aux.js", + "./vs/base/parts/sandbox/electron-sandbox/preload.js", + "./vs/code/electron-sandbox/processExplorer/processExplorer.js", + "./vs/code/electron-sandbox/workbench/workbench.js", + "./vs/workbench/contrib/issue/electron-sandbox/issueReporter.js", "./typings", "./vs/**/*.ts", "vscode-dts/vscode.proposed.*.d.ts", diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index 988f0485713..bad9fb8cacc 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -11,7 +11,7 @@ "moduleResolution": "classic", "removeComments": false, "preserveConstEnums": true, - "target": "es2018", + "target": "ES2022", "sourceMap": false, "declaration": true }, @@ -19,6 +19,7 @@ "typings/require.d.ts", "typings/thenable.d.ts", "typings/vscode-globals-product.d.ts", + "typings/vscode-globals-nls.d.ts", "vs/loader.d.ts", "vs/monaco.d.ts", "vs/editor/*", diff --git a/src/tsconfig.tsec.json b/src/tsconfig.tsec.json index d2524df22d4..d822b0a4e89 100644 --- a/src/tsconfig.tsec.json +++ b/src/tsconfig.tsec.json @@ -10,6 +10,7 @@ ] }, "exclude": [ + "./vs/workbench/contrib/webview/browser/pre/service-worker.js", "*/test/*", "**/*.test.ts" ] diff --git a/src/typings/require.d.ts b/src/typings/require.d.ts index f051253046f..7934279012f 100644 --- a/src/typings/require.d.ts +++ b/src/typings/require.d.ts @@ -3,30 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -declare enum LoaderEventType { - LoaderAvailable = 1, - - BeginLoadingScript = 10, - EndLoadingScriptOK = 11, - EndLoadingScriptError = 12, - - BeginInvokeFactory = 21, - EndInvokeFactory = 22, - - NodeBeginEvaluatingScript = 31, - NodeEndEvaluatingScript = 32, - - NodeBeginNativeRequire = 33, - NodeEndNativeRequire = 34, - - CachedDataFound = 60, - CachedDataMissed = 61, - CachedDataRejected = 62, - CachedDataCreated = 63, -} - declare class LoaderEvent { - readonly type: LoaderEventType; + readonly type: number; readonly timestamp: number; readonly detail: string; } diff --git a/src/typings/vscode-globals-nls.d.ts b/src/typings/vscode-globals-nls.d.ts new file mode 100644 index 00000000000..bc480767623 --- /dev/null +++ b/src/typings/vscode-globals-nls.d.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// AMD2ESM mirgation relevant + +/** + * NLS Globals: these need to be defined in all contexts that make + * use of our `nls.localize` and `nls.localize2` functions. This includes: + * - Electron main process + * - Electron window (renderer) process + * - Utility Process + * - Node.js + * - Browser + * - Web worker + * + * That is because during build time we strip out all english strings from + * the resulting JS code and replace it with a that is then looked + * up from the `_VSCODE_NLS_MESSAGES` array. + */ +declare global { + /** + * All NLS messages produced by `localize` and `localize2` calls + * under `src/vs` translated to the language as indicated by + * `_VSCODE_NLS_LANGUAGE`. + */ + var _VSCODE_NLS_MESSAGES: string[]; + /** + * The actual language of the NLS messages (e.g. 'en', de' or 'pt-br'). + */ + var _VSCODE_NLS_LANGUAGE: string | undefined; +} + +// fake export to make global work +export { } diff --git a/src/vs/base/browser/browser.ts b/src/vs/base/browser/browser.ts index 77f55b943a5..87db1c573ed 100644 --- a/src/vs/base/browser/browser.ts +++ b/src/vs/base/browser/browser.ts @@ -133,3 +133,9 @@ export function isStandalone(): boolean { export function isWCOEnabled(): boolean { return (navigator as any)?.windowControlsOverlay?.visible; } + +// Returns the bounding rect of the titlebar area if it is supported and defined +// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/getTitlebarAreaRect +export function getWCOBoundingRect(): DOMRect | undefined { + return (navigator as any)?.windowControlsOverlay?.getTitlebarAreaRect(); +} diff --git a/src/vs/base/browser/defaultWorkerFactory.ts b/src/vs/base/browser/defaultWorkerFactory.ts index 71391b063f3..834fa4d3c1a 100644 --- a/src/vs/base/browser/defaultWorkerFactory.ts +++ b/src/vs/base/browser/defaultWorkerFactory.ts @@ -50,27 +50,35 @@ export function getWorkerBootstrapUrl(scriptPath: string, label: string): string if (/^((http:)|(https:)|(file:))/.test(scriptPath) && scriptPath.substring(0, globalThis.origin.length) !== globalThis.origin) { // this is the cross-origin case // i.e. the webpage is running at a different origin than where the scripts are loaded from - const myPath = 'vs/base/worker/defaultWorkerFactory.js'; - const workerBaseUrl = require.toUrl(myPath).slice(0, -myPath.length); // explicitly using require.toUrl(), see https://github.com/microsoft/vscode/issues/107440#issuecomment-698982321 - const js = `/*${label}*/globalThis.MonacoEnvironment={baseUrl: '${workerBaseUrl}'};const ttPolicy = globalThis.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value });importScripts(ttPolicy?.createScriptURL('${scriptPath}') ?? '${scriptPath}');/*${label}*/`; - const blob = new Blob([js], { type: 'application/javascript' }); - return URL.createObjectURL(blob); - } - - const start = scriptPath.lastIndexOf('?'); - const end = scriptPath.lastIndexOf('#', start); - const params = start > 0 - ? new URLSearchParams(scriptPath.substring(start + 1, ~end ? end : undefined)) - : new URLSearchParams(); - - COI.addSearchParam(params, true, true); - const search = params.toString(); - - if (!search) { - return `${scriptPath}#${label}`; } else { - return `${scriptPath}?${params.toString()}#${label}`; + const start = scriptPath.lastIndexOf('?'); + const end = scriptPath.lastIndexOf('#', start); + const params = start > 0 + ? new URLSearchParams(scriptPath.substring(start + 1, ~end ? end : undefined)) + : new URLSearchParams(); + + COI.addSearchParam(params, true, true); + const search = params.toString(); + if (!search) { + scriptPath = `${scriptPath}#${label}`; + } else { + scriptPath = `${scriptPath}?${params.toString()}#${label}`; + } } + + const factoryModuleId = 'vs/base/worker/defaultWorkerFactory.js'; + const workerBaseUrl = require.toUrl(factoryModuleId).slice(0, -factoryModuleId.length); // explicitly using require.toUrl(), see https://github.com/microsoft/vscode/issues/107440#issuecomment-698982321 + const blob = new Blob([[ + `/*${label}*/`, + `globalThis.MonacoEnvironment = { baseUrl: '${workerBaseUrl}' };`, + // VSCODE_GLOBALS: NLS + `globalThis._VSCODE_NLS_MESSAGES = ${JSON.stringify(globalThis._VSCODE_NLS_MESSAGES)};`, + `globalThis._VSCODE_NLS_LANGUAGE = ${JSON.stringify(globalThis._VSCODE_NLS_LANGUAGE)};`, + `const ttPolicy = globalThis.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value });`, + `importScripts(ttPolicy?.createScriptURL('${scriptPath}') ?? '${scriptPath}');`, + `/*${label}*/` + ].join('')], { type: 'application/javascript' }); + return URL.createObjectURL(blob); } // ESM-comment-end @@ -136,8 +144,6 @@ class WebWorker extends Disposable implements IWorker { } }); } - - } export class DefaultWorkerFactory implements IWorkerFactory { diff --git a/src/vs/base/browser/dnd.ts b/src/vs/base/browser/dnd.ts index e55b238b08d..96259a4e63d 100644 --- a/src/vs/base/browser/dnd.ts +++ b/src/vs/base/browser/dnd.ts @@ -100,7 +100,7 @@ export function applyDragImage(event: DragEvent, label: string | null, clazz: st event.dataTransfer.setDragImage(dragImage, -10, -10); // Removes the element when the DND operation is done - setTimeout(() => ownerDocument.body.removeChild(dragImage), 0); + setTimeout(() => dragImage.remove(), 0); } } diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 76abbb844ec..a6acbe7f3a5 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -18,6 +18,7 @@ import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { hash } from 'vs/base/common/hash'; import { CodeWindow, ensureCodeWindow, mainWindow } from 'vs/base/browser/window'; +import { isPointWithinTriangle } from 'vs/base/common/numbers'; export interface IRegisteredCodeWindow { readonly window: CodeWindow; @@ -795,7 +796,7 @@ export function isAncestorUsingFlowTo(testChild: Node, testAncestor: Node): bool return true; } - if (node instanceof HTMLElement) { + if (isHTMLElement(node)) { const flowToParentElement = getParentFlowToElement(node); if (flowToParentElement) { node = flowToParentElement; @@ -967,7 +968,7 @@ export function createStyleSheet(container: HTMLElement = mainWindow.document.he container.appendChild(style); if (disposableStore) { - disposableStore.add(toDisposable(() => container.removeChild(style))); + disposableStore.add(toDisposable(() => style.remove())); } // With as container, the stylesheet becomes global and is tracked @@ -1004,7 +1005,7 @@ function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, globalStylesh const clone = globalStylesheet.cloneNode(true) as HTMLStyleElement; targetWindow.document.head.appendChild(clone); - disposables.add(toDisposable(() => targetWindow.document.head.removeChild(clone))); + disposables.add(toDisposable(() => clone.remove())); for (const rule of getDynamicStyleSheetRules(globalStylesheet)) { clone.sheet?.insertRule(rule.cssText, clone.sheet?.cssRules.length); @@ -1148,6 +1149,46 @@ function isCSSStyleRule(rule: CSSRule): rule is CSSStyleRule { return typeof (rule as CSSStyleRule).selectorText === 'string'; } +export function isHTMLElement(e: unknown): e is HTMLElement { + // eslint-disable-next-line no-restricted-syntax + return e instanceof HTMLElement || e instanceof getWindow(e as Node).HTMLElement; +} + +export function isHTMLAnchorElement(e: unknown): e is HTMLAnchorElement { + // eslint-disable-next-line no-restricted-syntax + return e instanceof HTMLAnchorElement || e instanceof getWindow(e as Node).HTMLAnchorElement; +} + +export function isHTMLSpanElement(e: unknown): e is HTMLSpanElement { + // eslint-disable-next-line no-restricted-syntax + return e instanceof HTMLSpanElement || e instanceof getWindow(e as Node).HTMLSpanElement; +} + +export function isHTMLTextAreaElement(e: unknown): e is HTMLTextAreaElement { + // eslint-disable-next-line no-restricted-syntax + return e instanceof HTMLTextAreaElement || e instanceof getWindow(e as Node).HTMLTextAreaElement; +} + +export function isHTMLInputElement(e: unknown): e is HTMLInputElement { + // eslint-disable-next-line no-restricted-syntax + return e instanceof HTMLInputElement || e instanceof getWindow(e as Node).HTMLInputElement; +} + +export function isHTMLButtonElement(e: unknown): e is HTMLButtonElement { + // eslint-disable-next-line no-restricted-syntax + return e instanceof HTMLButtonElement || e instanceof getWindow(e as Node).HTMLButtonElement; +} + +export function isHTMLDivElement(e: unknown): e is HTMLDivElement { + // eslint-disable-next-line no-restricted-syntax + return e instanceof HTMLDivElement || e instanceof getWindow(e as Node).HTMLDivElement; +} + +export function isSVGElement(e: unknown): e is SVGElement { + // eslint-disable-next-line no-restricted-syntax + return e instanceof SVGElement || e instanceof getWindow(e as Node).SVGElement; +} + export function isMouseEvent(e: unknown): e is MouseEvent { // eslint-disable-next-line no-restricted-syntax return e instanceof MouseEvent || e instanceof getWindow(e as UIEvent).MouseEvent; @@ -1286,7 +1327,7 @@ class FocusTracker extends Disposable implements IFocusTracker { private _refreshStateHandler: () => void; private static hasFocusWithin(element: HTMLElement | Window): boolean { - if (element instanceof HTMLElement) { + if (isHTMLElement(element)) { const shadowRoot = getShadowRoot(element); const activeElement = (shadowRoot ? shadowRoot.activeElement : element.ownerDocument.activeElement); return isAncestor(activeElement, element); @@ -1312,7 +1353,7 @@ class FocusTracker extends Disposable implements IFocusTracker { const onBlur = () => { if (hasFocus) { loosingFocus = true; - (element instanceof HTMLElement ? getWindow(element) : element).setTimeout(() => { + (isHTMLElement(element) ? getWindow(element) : element).setTimeout(() => { if (loosingFocus) { loosingFocus = false; hasFocus = false; @@ -1335,7 +1376,7 @@ class FocusTracker extends Disposable implements IFocusTracker { this._register(addDisposableListener(element, EventType.FOCUS, onFocus, true)); this._register(addDisposableListener(element, EventType.BLUR, onBlur, true)); - if (element instanceof HTMLElement) { + if (isHTMLElement(element)) { this._register(addDisposableListener(element, EventType.FOCUS_IN, () => this._refreshStateHandler())); this._register(addDisposableListener(element, EventType.FOCUS_OUT, () => this._refreshStateHandler())); } @@ -1488,7 +1529,7 @@ export function hide(...elements: HTMLElement[]): void { function findParentWithAttribute(node: Node | null, attribute: string): HTMLElement | null { while (node && node.nodeType === node.ELEMENT_NODE) { - if (node instanceof HTMLElement && node.hasAttribute(attribute)) { + if (isHTMLElement(node) && node.hasAttribute(attribute)) { return node; } @@ -1691,7 +1732,7 @@ export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void anchor.click(); // Ensure to remove the element from DOM eventually - setTimeout(() => activeWindow.document.body.removeChild(anchor)); + setTimeout(() => anchor.remove()); } export function triggerUpload(): Promise { @@ -1714,7 +1755,7 @@ export function triggerUpload(): Promise { input.click(); // Ensure to remove the element from DOM eventually - setTimeout(() => activeWindow.document.body.removeChild(input)); + setTimeout(() => input.remove()); }); } @@ -2304,7 +2345,108 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia if (children) { for (const c of children) { - if (c instanceof HTMLElement) { + if (isHTMLElement(c)) { + el.appendChild(c); + } else if (typeof c === 'string') { + el.append(c); + } else if ('root' in c) { + Object.assign(result, c); + el.appendChild(c.root); + } + } + } + + for (const [key, value] of Object.entries(attributes)) { + if (key === 'className') { + continue; + } else if (key === 'style') { + for (const [cssKey, cssValue] of Object.entries(value)) { + el.style.setProperty( + camelCaseToHyphenCase(cssKey), + typeof cssValue === 'number' ? cssValue + 'px' : '' + cssValue + ); + } + } else if (key === 'tabIndex') { + el.tabIndex = value; + } else { + el.setAttribute(camelCaseToHyphenCase(key), value.toString()); + } + } + + result['root'] = el; + + return result; +} + +export function svgElem + (tag: TTag): + TagToRecord extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function svgElem + (tag: TTag, children: [...T]): + (ArrayToObj & TagToRecord) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function svgElem + (tag: TTag, attributes: Partial>>): + TagToRecord extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function svgElem + (tag: TTag, attributes: Partial>>, children: [...T]): + (ArrayToObj & TagToRecord) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function svgElem(tag: string, ...args: [] | [attributes: { $: string } & Partial> | Record, children?: any[]] | [children: any[]]): Record { + let attributes: { $?: string } & Partial>; + let children: (Record | HTMLElement)[] | undefined; + + if (Array.isArray(args[0])) { + attributes = {}; + children = args[0]; + } else { + attributes = args[0] as any || {}; + children = args[1]; + } + + const match = H_REGEX.exec(tag); + + if (!match || !match.groups) { + throw new Error('Bad use of h'); + } + + const tagName = match.groups['tag'] || 'div'; + const el = document.createElementNS('http://www.w3.org/2000/svg', tagName) as any as HTMLElement; + + if (match.groups['id']) { + el.id = match.groups['id']; + } + + const classNames = []; + if (match.groups['class']) { + for (const className of match.groups['class'].split('.')) { + if (className !== '') { + classNames.push(className); + } + } + } + if (attributes.className !== undefined) { + for (const className of attributes.className.split('.')) { + if (className !== '') { + classNames.push(className); + } + } + } + if (classNames.length > 0) { + el.className = classNames.join(' '); + } + + const result: Record = {}; + + if (match.groups['name']) { + result[match.groups['name']] = el; + } + + if (children) { + for (const c of children) { + if (isHTMLElement(c)) { el.appendChild(c); } else if (typeof c === 'string') { el.append(c); @@ -2373,3 +2515,53 @@ export function trackAttributes(from: Element, to: Element, filter?: string[]): return disposables; } + +/** + * Helper for calculating the "safe triangle" occluded by hovers to avoid early dismissal. + * @see https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/ for example + */ +export class SafeTriangle { + // 4 triangles, 2 points (x, y) stored for each + private triangles: number[] = []; + + constructor( + private readonly originX: number, + private readonly originY: number, + target: HTMLElement + ) { + const { top, left, right, bottom } = target.getBoundingClientRect(); + const t = this.triangles; + let i = 0; + + t[i++] = left; + t[i++] = top; + t[i++] = right; + t[i++] = top; + + t[i++] = left; + t[i++] = top; + t[i++] = left; + t[i++] = bottom; + + t[i++] = right; + t[i++] = top; + t[i++] = right; + t[i++] = bottom; + + t[i++] = left; + t[i++] = bottom; + t[i++] = right; + t[i++] = bottom; + } + + public contains(x: number, y: number) { + const { triangles, originX, originY } = this; + for (let i = 0; i < 4; i++) { + if (isPointWithinTriangle(x, y, originX, originY, triangles[2 * i], triangles[2 * i + 1], triangles[2 * i + 2], triangles[2 * i + 3])) { + return true; + } + } + + return false; + } +} diff --git a/src/vs/base/browser/domObservable.ts b/src/vs/base/browser/domObservable.ts new file mode 100644 index 00000000000..dd20637727b --- /dev/null +++ b/src/vs/base/browser/domObservable.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createStyleSheet2 } from 'vs/base/browser/dom'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { autorun, IObservable } from 'vs/base/common/observable'; + +export function createStyleSheetFromObservable(css: IObservable): IDisposable { + const store = new DisposableStore(); + const w = store.add(createStyleSheet2()); + store.add(autorun(reader => { + w.setStyle(css.read(reader)); + })); + return store; +} diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index b1a304845a3..265a57113f7 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -427,7 +427,7 @@ function sanitizeRenderedMarkdown( if (element.attributes.getNamedItem('type')?.value === 'checkbox') { element.setAttribute('disabled', ''); } else if (!options.replaceWithPlaintext) { - element.parentElement?.removeChild(element); + element.remove(); } } @@ -482,6 +482,7 @@ export const allowedMarkdownAttr = [ 'alt', 'checked', 'class', + 'colspan', 'controls', 'data-code', 'data-href', @@ -493,6 +494,7 @@ export const allowedMarkdownAttr = [ 'muted', 'playsinline', 'poster', + 'rowspan', 'src', 'style', 'target', @@ -634,7 +636,7 @@ const plainTextRenderer = new Lazy((withCodeBlocks?: boolean) = const plainTextWithCodeBlocksRenderer = new Lazy(() => { const renderer = createRenderer(); renderer.code = (code: string): string => { - return '\n' + '```' + code + '```' + '\n'; + return `\n\`\`\`\n${code}\n\`\`\`\n`; }; return renderer; }); @@ -766,8 +768,8 @@ function completeListItemPattern(list: marked.Tokens.List): marked.Tokens.List | const previousListItemsText = mergeRawTokenText(list.items.slice(0, -1)); - // Grabbing the `- ` or `1. ` off the list item because I can't find a better way to do this - const lastListItemLead = lastListItem.raw.match(/^(\s*(-|\d+\.) +)/)?.[0]; + // Grabbing the `- ` or `1. ` or `* ` off the list item because I can't find a better way to do this + const lastListItemLead = lastListItem.raw.match(/^(\s*(-|\d+\.|\*) +)/)?.[0]; if (!lastListItemLead) { // Is badly formatted return; diff --git a/src/vs/base/browser/trustedTypes.ts b/src/vs/base/browser/trustedTypes.ts index 48c02ca8c97..0ef4b084528 100644 --- a/src/vs/base/browser/trustedTypes.ts +++ b/src/vs/base/browser/trustedTypes.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mainWindow } from 'vs/base/browser/window'; import { onUnexpectedError } from 'vs/base/common/errors'; export function createTrustedTypesPolicy( @@ -28,7 +27,7 @@ export function createTrustedTypesPolicy= 0 && index < this.viewItems.length) { - this.actionsList.removeChild(this.actionsList.childNodes[index]); + this.actionsList.childNodes[index].remove(); this.viewItemDisposables.deleteAndDispose(this.viewItems[index]); dispose(this.viewItems.splice(index, 1)); this.refreshRole(); diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 3c42632c500..30f588f4d49 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -22,7 +22,7 @@ import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecyc import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./button'; import { localize } from 'vs/nls'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { IActionProvider } from 'vs/base/browser/ui/dropdown/dropdown'; @@ -80,7 +80,7 @@ export class Button extends Disposable implements IButton { protected _label: string | IMarkdownString = ''; protected _labelElement: HTMLElement | undefined; protected _labelShortElement: HTMLElement | undefined; - private _hover: IUpdatableHover | undefined; + private _hover: IManagedHover | undefined; private _onDidClick = this._register(new Emitter()); get onDidClick(): BaseEvent { return this._onDidClick.event; } @@ -306,7 +306,7 @@ export class Button extends Disposable implements IButton { setTitle(title: string) { if (!this._hover && title !== '') { - this._hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this._element, title)); + this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this._element, title)); } else if (this._hover) { this._hover.update(title); } @@ -370,7 +370,7 @@ export class ButtonWithDropdown extends Disposable implements IButton { this.separator.style.backgroundColor = options.buttonSeparator ?? ''; this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true })); - this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.dropdownButton.element, localize("button dropdown more actions", 'More Actions...'))); + this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.dropdownButton.element, localize("button dropdown more actions", 'More Actions...'))); this.dropdownButton.element.setAttribute('aria-haspopup', 'true'); this.dropdownButton.element.setAttribute('aria-expanded', 'false'); this.dropdownButton.element.classList.add('monaco-dropdown-button'); diff --git a/src/vs/base/browser/ui/centered/centeredViewLayout.ts b/src/vs/base/browser/ui/centered/centeredViewLayout.ts index db13907259a..b6bc80b1d99 100644 --- a/src/vs/base/browser/ui/centered/centeredViewLayout.ts +++ b/src/vs/base/browser/ui/centered/centeredViewLayout.ts @@ -166,7 +166,7 @@ export class CenteredViewLayout implements IDisposable { } if (active) { - this.container.removeChild(this.view.element); + this.view.element.remove(); this.splitView = new SplitView(this.container, { inverseAltBehavior: true, orientation: Orientation.HORIZONTAL, @@ -195,9 +195,7 @@ export class CenteredViewLayout implements IDisposable { this.resizeSplitViews(); } else { - if (this.splitView) { - this.container.removeChild(this.splitView.el); - } + this.splitView?.el.remove(); this.splitViewDisposables.clear(); this.splitView?.dispose(); this.splitView = undefined; diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index c10dda88f89..1edda5b714c 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -169,13 +169,11 @@ export class ContextView extends Disposable { if (this.container) { this.toDisposeOnSetContainer.dispose(); + this.view.remove(); if (this.shadowRoot) { - this.shadowRoot.removeChild(this.view); this.shadowRoot = null; this.shadowRootHostElement?.remove(); this.shadowRootHostElement = null; - } else { - this.container.removeChild(this.view); } this.container = null; @@ -274,7 +272,7 @@ export class ContextView extends Disposable { let around: IView; // Get the element's position and size (to anchor the view) - if (anchor instanceof HTMLElement) { + if (DOM.isHTMLElement(anchor)) { const elementPosition = DOM.getDomNodePagePosition(anchor); // In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 1089d8275ba..ba003576019 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -8,7 +8,7 @@ import { $, addDisposableListener, append, EventHelper, EventType, isMouseEvent import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as GestureEventType, Gesture } from 'vs/base/browser/touch'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IMenuOptions } from 'vs/base/browser/ui/menu/menu'; @@ -37,7 +37,7 @@ class BaseDropdown extends ActionRunner { private _onDidChangeVisibility = this._register(new Emitter()); readonly onDidChangeVisibility = this._onDidChangeVisibility.event; - private hover: IUpdatableHover | undefined; + private hover: IManagedHover | undefined; constructor(container: HTMLElement, options: IBaseDropdownOptions) { super(); @@ -107,7 +107,7 @@ class BaseDropdown extends ActionRunner { set tooltip(tooltip: string) { if (this._label) { if (!this.hover && tooltip !== '') { - this.hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this._label, tooltip)); + this.hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this._label, tooltip)); } else if (this.hover) { this.hover.update(tooltip); } diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 18cfd87d2bc..007e1de2a66 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -93,7 +93,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { this.element.setAttribute('aria-haspopup', 'true'); this.element.setAttribute('aria-expanded', 'false'); if (this._action.label) { - this._register(getBaseLayerHoverDelegate().setupUpdatableHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.element, this._action.label)); + this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.element, this._action.label)); } this.element.ariaLabel = this._action.label || ''; diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 1d7eacf9fdc..17b24550622 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -1063,7 +1063,7 @@ export class GridView implements IDisposable { const oldRoot = this._root; if (oldRoot) { - this.element.removeChild(oldRoot.element); + oldRoot.element.remove(); oldRoot.dispose(); } @@ -1831,6 +1831,6 @@ export class GridView implements IDisposable { dispose(): void { this.onDidSashResetRelay.dispose(); this.root.dispose(); - this.element.parentElement?.removeChild(this.element); + this.element.remove(); } } diff --git a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts index 724075adb87..83b0c26bcae 100644 --- a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts +++ b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; @@ -43,7 +43,7 @@ export class HighlightedLabel extends Disposable { private highlights: readonly IHighlight[] = []; private supportIcons: boolean; private didEverRender: boolean = false; - private customHover: IUpdatableHover | undefined; + private customHover: IManagedHover | undefined; /** * Create a new {@link HighlightedLabel}. @@ -141,7 +141,7 @@ export class HighlightedLabel extends Disposable { } else { if (!this.customHover && this.title !== '') { const hoverDelegate = this.options?.hoverDelegate ?? getDefaultHoverDelegate('mouse'); - this.customHover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(hoverDelegate, this.domNode, this.title)); + this.customHover = this._register(getBaseLayerHoverDelegate().setupManagedHover(hoverDelegate, this.domNode, this.title)); } else if (this.customHover) { this.customHover.update(this.title); } diff --git a/src/vs/base/browser/ui/hover/hover.ts b/src/vs/base/browser/ui/hover/hover.ts index f2b7582d7fa..f66ba1ea673 100644 --- a/src/vs/base/browser/ui/hover/hover.ts +++ b/src/vs/base/browser/ui/hover/hover.ts @@ -14,11 +14,12 @@ import type { IDisposable } from 'vs/base/common/lifecycle'; */ export interface IHoverDelegate2 { /** - * Shows a hover, provided a hover with the same options object is not already visible. + * Shows a hover, provided a hover with the same {@link options} object is not already visible. + * * @param options A set of options defining the characteristics of the hover. * @param focus Whether to focus the hover (useful for keyboard accessibility). * - * **Example:** A simple usage with a single element target. + * @example A simple usage with a single element target. * * ```typescript * showHover({ @@ -27,7 +28,10 @@ export interface IHoverDelegate2 { * }); * ``` */ - showHover(options: IHoverOptions, focus?: boolean): IHoverWidget | undefined; + showHover( + options: IHoverOptions, + focus?: boolean + ): IHoverWidget | undefined; /** * Hides the hover if it was visible. This call will be ignored if the the hover is currently @@ -41,16 +45,37 @@ export interface IHoverDelegate2 { */ showAndFocusLastHover(): void; - // TODO: Change hoverDelegate arg to exclude the actual delegate and instead use the new options - setupUpdatableHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IUpdatableHoverContentOrFactory, options?: IUpdatableHoverOptions): IUpdatableHover; + /** + * Sets up a managed hover for the given element. A managed hover will set up listeners for + * mouse events, show the hover after a delay and provide hooks to easily update the content. + * + * This should be used over {@link showHover} when fine-grained control is not needed. The + * managed hover also does not scale well, consider using {@link showHover} when showing hovers + * for many elements. + * + * @param hoverDelegate The hover delegate containing hooks and configuration for the hover. + * @param targetElement The target element to show the hover for. + * @param content The content of the hover or a factory that creates it at the time it's shown. + * @param options Additional options for the managed hover. + */ + // TODO: The hoverDelegate parameter should be removed in favor of just a set of options. This + // will avoid confusion around IHoverDelegate/IHoverDelegate2 as well as align more with + // the design of the hover service. + // TODO: Align prototype closer to showHover, deriving options from IHoverOptions if possible. + setupManagedHover(hoverDelegate: IHoverDelegate, targetElement: HTMLElement, content: IManagedHoverContentOrFactory, options?: IManagedHoverOptions): IManagedHover; /** * Shows the hover for the given element if one has been setup. + * + * @param targetElement The target element of the hover, as set up in {@link setupManagedHover}. */ - triggerUpdatableHover(htmlElement: HTMLElement): void; + showManagedHover(targetElement: HTMLElement): void; } export interface IHoverWidget extends IDisposable { + /** + * Whether the hover widget has been disposed. + */ readonly isDisposed: boolean; } @@ -229,33 +254,29 @@ export interface IHoverTarget extends IDisposable { * An optional absolute x coordinate to position the hover with, for example to position the * hover using `MouseEvent.pageX`. */ - x?: number; + readonly x?: number; /** * An optional absolute y coordinate to position the hover with, for example to position the * hover using `MouseEvent.pageY`. */ - y?: number; + readonly y?: number; } -// #region Updatable hover +// #region Managed hover -export interface IUpdatableHoverTooltipMarkdownString { +export interface IManagedHoverTooltipMarkdownString { markdown: IMarkdownString | string | undefined | ((token: CancellationToken) => Promise); markdownNotSupportedFallback: string | undefined; } -export type IUpdatableHoverContent = string | IUpdatableHoverTooltipMarkdownString | HTMLElement | undefined; -export type IUpdatableHoverContentOrFactory = IUpdatableHoverContent | (() => IUpdatableHoverContent); +export type IManagedHoverContent = string | IManagedHoverTooltipMarkdownString | HTMLElement | undefined; +export type IManagedHoverContentOrFactory = IManagedHoverContent | (() => IManagedHoverContent); -export interface IUpdatableHoverOptions { - actions?: IHoverAction[]; - linkHandler?(url: string): void; - trapFocus?: boolean; +export interface IManagedHoverOptions extends Pick { } -export interface IUpdatableHover extends IDisposable { - +export interface IManagedHover extends IDisposable { /** * Allows to programmatically open the hover. */ @@ -269,7 +290,7 @@ export interface IUpdatableHover extends IDisposable { /** * Updates the contents of the hover. */ - update(tooltip: IUpdatableHoverContent, options?: IUpdatableHoverOptions): void; + update(tooltip: IManagedHoverContent, options?: IManagedHoverOptions): void; } -// #endregion Updatable hover +// #endregion Managed hover diff --git a/src/vs/base/browser/ui/hover/hoverDelegate.ts b/src/vs/base/browser/ui/hover/hoverDelegate.ts index d2f1d7884ff..47ea1b77531 100644 --- a/src/vs/base/browser/ui/hover/hoverDelegate.ts +++ b/src/vs/base/browser/ui/hover/hoverDelegate.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IHoverWidget, IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import type { IHoverWidget, IManagedHoverOptions } from 'vs/base/browser/ui/hover/hover'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -13,7 +13,7 @@ export interface IHoverDelegateTarget extends IDisposable { x?: number; } -export interface IHoverDelegateOptions extends IUpdatableHoverOptions { +export interface IHoverDelegateOptions extends IManagedHoverOptions { /** * The content to display in the primary section of the hover. The type of text determines the * default `hideOnHover` behavior. diff --git a/src/vs/base/browser/ui/hover/hoverDelegate2.ts b/src/vs/base/browser/ui/hover/hoverDelegate2.ts index 13a379222c1..1d6a312f70d 100644 --- a/src/vs/base/browser/ui/hover/hoverDelegate2.ts +++ b/src/vs/base/browser/ui/hover/hoverDelegate2.ts @@ -9,8 +9,8 @@ let baseHoverDelegate: IHoverDelegate2 = { showHover: () => undefined, hideHover: () => undefined, showAndFocusLastHover: () => undefined, - setupUpdatableHover: () => null!, - triggerUpdatableHover: () => undefined + setupManagedHover: () => null!, + showManagedHover: () => undefined }; /** diff --git a/src/vs/base/browser/ui/hover/hoverWidget.css b/src/vs/base/browser/ui/hover/hoverWidget.css index 094b16e58be..a1b3b43211e 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.css +++ b/src/vs/base/browser/ui/hover/hoverWidget.css @@ -134,6 +134,11 @@ padding-right: 4px; } +.monaco-hover .hover-row.status-bar .actions .action-container a { + color: var(--vscode-textLink-foreground); + text-decoration: var(--text-link-decoration); +} + .monaco-hover .markdown-hover .hover-contents .codicon { color: inherit; font-size: inherit; @@ -171,12 +176,6 @@ display: inline-block; } -.monaco-hover-content { - padding-right: 2px; - padding-bottom: 2px; - box-sizing: border-box; -} - .monaco-hover-content .action-container a { -webkit-user-select: none; user-select: none; diff --git a/src/vs/base/browser/ui/hover/hoverWidget.ts b/src/vs/base/browser/ui/hover/hoverWidget.ts index 2e9ecbdd1fe..9d836121267 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.ts +++ b/src/vs/base/browser/ui/hover/hoverWidget.ts @@ -50,12 +50,18 @@ export class HoverAction extends Disposable { return new HoverAction(parent, actionOptions, keybindingLabel); } + public readonly actionLabel: string; + public readonly actionKeybindingLabel: string | null; + private readonly actionContainer: HTMLElement; private readonly action: HTMLElement; private constructor(parent: HTMLElement, actionOptions: { label: string; iconClass?: string; run: (target: HTMLElement) => void; commandId: string }, keybindingLabel: string | null) { super(); + this.actionLabel = actionOptions.label; + this.actionKeybindingLabel = keybindingLabel; + this.actionContainer = dom.append(parent, $('div.action-container')); this.actionContainer.setAttribute('tabindex', '0'); diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 2b297fa6def..e761b71228e 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -12,7 +12,7 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { equals } from 'vs/base/common/objects'; import { Range } from 'vs/base/common/range'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import type { IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { isString } from 'vs/base/common/types'; import { stripIcons } from 'vs/base/common/iconLabels'; @@ -26,8 +26,8 @@ export interface IIconLabelCreationOptions { } export interface IIconLabelValueOptions { - title?: string | IUpdatableHoverTooltipMarkdownString; - descriptionTitle?: string | IUpdatableHoverTooltipMarkdownString; + title?: string | IManagedHoverTooltipMarkdownString; + descriptionTitle?: string | IManagedHoverTooltipMarkdownString; suffix?: string; hideIcon?: boolean; extraClasses?: readonly string[]; @@ -158,7 +158,7 @@ export class IconLabel extends Disposable { const existingIconNode = this.domNode.element.querySelector('.monaco-icon-label-iconpath'); if (options?.iconPath) { let iconNode; - if (!existingIconNode || !(existingIconNode instanceof HTMLElement)) { + if (!existingIconNode || !(dom.isHTMLElement(existingIconNode))) { iconNode = dom.$('.monaco-icon-label-iconpath'); this.domNode.element.prepend(iconNode); } else { @@ -194,7 +194,7 @@ export class IconLabel extends Disposable { } } - private setupHover(htmlElement: HTMLElement, tooltip: string | IUpdatableHoverTooltipMarkdownString | undefined): void { + private setupHover(htmlElement: HTMLElement, tooltip: string | IManagedHoverTooltipMarkdownString | undefined): void { const previousCustomHover = this.customHovers.get(htmlElement); if (previousCustomHover) { previousCustomHover.dispose(); @@ -207,7 +207,7 @@ export class IconLabel extends Disposable { } if (this.hoverDelegate.showNativeHover) { - function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IUpdatableHoverTooltipMarkdownString | undefined): void { + function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IManagedHoverTooltipMarkdownString | undefined): void { if (isString(tooltip)) { // Icons don't render in the native hover so we strip them out htmlElement.title = stripIcons(tooltip); @@ -219,7 +219,7 @@ export class IconLabel extends Disposable { } setupNativeHover(htmlElement, tooltip); } else { - const hoverDisposable = getBaseLayerHoverDelegate().setupUpdatableHover(this.hoverDelegate, htmlElement, tooltip); + const hoverDisposable = getBaseLayerHoverDelegate().setupManagedHover(this.hoverDelegate, htmlElement, tooltip); if (hoverDisposable) { this.customHovers.set(htmlElement, hoverDisposable); } diff --git a/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts b/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts index 6f960b8add0..ea8179ad642 100644 --- a/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { reset } from 'vs/base/browser/dom'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; @@ -12,7 +12,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; export class SimpleIconLabel implements IDisposable { - private hover?: IUpdatableHover; + private hover?: IManagedHover; constructor( private readonly _container: HTMLElement @@ -24,7 +24,7 @@ export class SimpleIconLabel implements IDisposable { set title(title: string) { if (!this.hover && title) { - this.hover = getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this._container, title); + this.hover = getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this._container, title); } else if (this.hover) { this.hover.update(title); } diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index e4215ad7642..870afb65c0a 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -11,7 +11,7 @@ import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -114,7 +114,7 @@ export class InputBox extends Widget { private cachedContentHeight: number | undefined; private maxHeight: number = Number.POSITIVE_INFINITY; private scrollableElement: ScrollableElement | undefined; - private hover: IUpdatableHover | undefined; + private hover: IManagedHover | undefined; private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; @@ -235,7 +235,7 @@ export class InputBox extends Widget { public setTooltip(tooltip: string): void { this.tooltip = tooltip; if (!this.hover) { - this.hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.input, tooltip)); + this.hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.input, tooltip)); } else { this.hover.update(tooltip); } diff --git a/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts b/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts index b6c8e1e4db1..189317b48e8 100644 --- a/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts +++ b/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { UILabelProvider } from 'vs/base/common/keybindingLabels'; @@ -61,7 +61,7 @@ export class KeybindingLabel extends Disposable { private readonly keyElements = new Set(); - private hover: IUpdatableHover; + private hover: IManagedHover; private keybinding: ResolvedKeybinding | undefined; private matches: Matches | undefined; private didEverRender: boolean; @@ -78,7 +78,7 @@ export class KeybindingLabel extends Disposable { this.domNode.style.color = labelForeground; } - this.hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.domNode, '')); + this.hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.domNode, '')); this.didEverRender = false; container.appendChild(this.domNode); diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 1550e7cc10e..1dcd981388f 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd'; -import { $, addDisposableListener, animate, Dimension, getContentHeight, getContentWidth, getTopLeftOffset, getWindow, isAncestor, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { $, addDisposableListener, animate, Dimension, getContentHeight, getContentWidth, getTopLeftOffset, getWindow, isAncestor, isHTMLElement, isSVGElement, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { EventType as TouchEventType, Gesture, GestureEvent } from 'vs/base/browser/touch'; @@ -1158,7 +1158,7 @@ export class ListView implements IListView { const container = getDragImageContainer(this.domNode); container.appendChild(dragImage); event.dataTransfer.setDragImage(dragImage, -10, -10); - setTimeout(() => container.removeChild(dragImage), 0); + setTimeout(() => dragImage.remove(), 0); } this.domNode.classList.add('dragging'); @@ -1382,9 +1382,9 @@ export class ListView implements IListView { private getItemIndexFromEventTarget(target: EventTarget | null): number | undefined { const scrollableElement = this.scrollableElement.getDomNode(); - let element: HTMLElement | null = target as (HTMLElement | null); + let element: HTMLElement | SVGElement | null = target as (HTMLElement | SVGElement | null); - while (element instanceof HTMLElement && element !== this.rowsContainer && scrollableElement.contains(element)) { + while ((isHTMLElement(element) || isSVGElement(element)) && element !== this.rowsContainer && scrollableElement.contains(element)) { const rawIndex = element.getAttribute('data-index'); if (rawIndex) { @@ -1542,7 +1542,7 @@ export class ListView implements IListView { this.virtualDelegate.setDynamicHeight?.(item.element, item.size); item.lastDynamicHeightWidth = this.renderWidth; - this.rowsContainer.removeChild(row.domNode); + row.domNode.remove(); this.cache.release(row); return item.size - size; @@ -1570,9 +1570,7 @@ export class ListView implements IListView { this.items = []; - if (this.domNode && this.domNode.parentNode) { - this.domNode.parentNode.removeChild(this.domNode); - } + this.domNode?.remove(); this.dragOverAnimationDisposable?.dispose(); this.disposables.dispose(); diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 576a0bfa640..535b245d71d 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IDragAndDropData } from 'vs/base/browser/dnd'; -import { asCssValueWithDefault, createStyleSheet, Dimension, EventHelper, getActiveElement, getWindow, isActiveElement, isMouseEvent } from 'vs/base/browser/dom'; +import { asCssValueWithDefault, createStyleSheet, Dimension, EventHelper, getActiveElement, getWindow, isActiveElement, isHTMLElement, isMouseEvent } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Gesture } from 'vs/base/browser/touch'; @@ -635,7 +635,7 @@ class DOMFocusController implements IDisposable { const tabIndexElement = focusedDomElement.querySelector('[tabIndex]'); - if (!tabIndexElement || !(tabIndexElement instanceof HTMLElement) || tabIndexElement.tabIndex === -1) { + if (!tabIndexElement || !(isHTMLElement(tabIndexElement)) || tabIndexElement.tabIndex === -1) { return; } diff --git a/src/vs/base/browser/ui/list/rowCache.ts b/src/vs/base/browser/ui/list/rowCache.ts index f71bdfd01f3..ff605b097ce 100644 --- a/src/vs/base/browser/ui/list/rowCache.ts +++ b/src/vs/base/browser/ui/list/rowCache.ts @@ -13,14 +13,6 @@ export interface IRow { templateData: any; } -function removeFromParent(element: HTMLElement): void { - try { - element.parentElement?.removeChild(element); - } catch (e) { - // this will throw if this happens due to a blur event, nasty business - } -} - export class RowCache implements IDisposable { private cache = new Map(); @@ -104,7 +96,7 @@ export class RowCache implements IDisposable { private doRemoveNode(domNode: HTMLElement) { domNode.classList.remove('scrolling'); - removeFromParent(domNode); + domNode.remove(); } private getTemplateCache(templateId: string): IRow[] { diff --git a/src/vs/base/browser/ui/menu/menubar.ts b/src/vs/base/browser/ui/menu/menubar.ts index 961fb8862a8..7af34aaafde 100644 --- a/src/vs/base/browser/ui/menu/menubar.ts +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -1010,8 +1010,8 @@ export class MenuBar extends Disposable { if (this.options.compactMode?.horizontal === HorizontalDirection.Right) { menuHolder.style.left = `${titleBoundingRect.left + this.container.clientWidth}px`; } else if (this.options.compactMode?.horizontal === HorizontalDirection.Left) { - menuHolder.style.top = `${titleBoundingRect.top}px`; - menuHolder.style.right = `${this.container.clientWidth}px`; + const windowWidth = DOM.getWindow(this.container).innerWidth; + menuHolder.style.right = `${windowWidth - titleBoundingRect.left}px`; menuHolder.style.left = 'auto'; } else { menuHolder.style.left = `${titleBoundingRect.left * titleBoundingRectZoom}px`; diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index 44fa17be826..210b98af009 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, append, createStyleSheet, EventHelper, EventLike, getWindow } from 'vs/base/browser/dom'; +import { $, append, createStyleSheet, EventHelper, EventLike, getWindow, isHTMLElement } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { EventType, Gesture } from 'vs/base/browser/touch'; import { Delayer } from 'vs/base/common/async'; @@ -575,7 +575,7 @@ export class Sash extends Disposable { const onPointerUp = (e: PointerEvent) => { EventHelper.stop(e, false); - this.el.removeChild(style); + style.remove(); this.el.classList.remove('active'); this._onDidEnd.fire(); @@ -670,7 +670,7 @@ export class Sash extends Disposable { private getOrthogonalSash(e: PointerEvent): Sash | undefined { const target = e.initialTarget ?? e.target; - if (!target || !(target instanceof HTMLElement)) { + if (!target || !(isHTMLElement(target))) { return undefined; } diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index a58782d95df..64bdfa22cb4 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -9,7 +9,7 @@ import { IContentActionHandler } from 'vs/base/browser/formattedTextRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { AnchorPosition, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IListEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; @@ -104,7 +104,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi private selectionDetailsPane!: HTMLElement; private _skipLayout: boolean = false; private _cachedMaxDetailsHeight?: number; - private _hover?: IUpdatableHover; + private _hover?: IManagedHover; private _sticky: boolean = false; // for dev purposes only @@ -153,7 +153,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi private setTitle(title: string): void { if (!this._hover && title) { - this._hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.selectElement, title)); + this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.selectElement, title)); } else if (this._hover) { this._hover.update(title); } @@ -520,12 +520,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi return { dispose: () => { // contextView will dispose itself if moving from one View to another - try { - container.removeChild(this.selectDropDownContainer); // remove to take out the CSS rules we add - } - catch (error) { - // Ignore, removed already by change of focus - } + this.selectDropDownContainer.remove(); // remove to take out the CSS rules we add } }; } @@ -612,8 +607,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi && this.options.length > maxVisibleOptionsBelow ) { this._dropDownPosition = AnchorPosition.ABOVE; - this.selectDropDownContainer.removeChild(this.selectDropDownListContainer); - this.selectDropDownContainer.removeChild(this.selectionDetailsPane); + this.selectDropDownListContainer.remove(); + this.selectionDetailsPane.remove(); this.selectDropDownContainer.appendChild(this.selectionDetailsPane); this.selectDropDownContainer.appendChild(this.selectDropDownListContainer); @@ -622,8 +617,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } else { this._dropDownPosition = AnchorPosition.BELOW; - this.selectDropDownContainer.removeChild(this.selectDropDownListContainer); - this.selectDropDownContainer.removeChild(this.selectionDetailsPane); + this.selectDropDownListContainer.remove(); + this.selectionDetailsPane.remove(); this.selectDropDownContainer.appendChild(this.selectDropDownListContainer); this.selectDropDownContainer.appendChild(this.selectionDetailsPane); @@ -879,7 +874,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi const tagName = child.tagName && child.tagName.toLowerCase(); if (tagName === 'img') { - element.removeChild(child); + child.remove(); } else { cleanRenderedMarkdown(child); } diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index 0b519e43411..6a78dee3860 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -5,7 +5,7 @@ import { isFirefox } from 'vs/base/browser/browser'; import { DataTransfers } from 'vs/base/browser/dnd'; -import { $, addDisposableListener, append, clearNode, EventHelper, EventType, getWindow, trackFocus } from 'vs/base/browser/dom'; +import { $, addDisposableListener, append, clearNode, EventHelper, EventType, getWindow, isHTMLElement, trackFocus } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch'; @@ -382,7 +382,7 @@ class PaneDraggable extends Disposable { const dragImage = append(this.pane.element.ownerDocument.body, $('.monaco-drag-image', {}, this.pane.draggableElement.textContent || '')); e.dataTransfer.setDragImage(dragImage, -10, -10); - setTimeout(() => this.pane.element.ownerDocument.body.removeChild(dragImage), 0); + setTimeout(() => dragImage.remove(), 0); this.context.draggable = this; } @@ -513,7 +513,7 @@ export class PaneView extends Disposable { const eventDisposables = this._register(new DisposableStore()); const onKeyDown = this._register(new DomEmitter(this.element, 'keydown')); - const onHeaderKeyDown = Event.map(Event.filter(onKeyDown.event, e => e.target instanceof HTMLElement && e.target.classList.contains('pane-header'), eventDisposables), e => new StandardKeyboardEvent(e), eventDisposables); + const onHeaderKeyDown = Event.map(Event.filter(onKeyDown.event, e => isHTMLElement(e.target) && e.target.classList.contains('pane-header'), eventDisposables), e => new StandardKeyboardEvent(e), eventDisposables); this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === KeyCode.UpArrow, eventDisposables)(() => this.focusPrevious())); this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === KeyCode.DownArrow, eventDisposables)(() => this.focusNext())); diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index ae7ad6708e4..ac966adf435 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -1127,7 +1127,7 @@ export class SplitView this.onViewChange(item, size)); - const containerDisposable = toDisposable(() => this.viewContainer.removeChild(container)); + const containerDisposable = toDisposable(() => container.remove()); const disposable = combinedDisposable(onChangeDisposable, containerDisposable); let viewSize: ViewItemSize; diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 631c0015d4b..b2c8959c3c5 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -134,7 +134,7 @@ class ColumnHeader extends Disposable implements IView { this.element = $('.monaco-table-th', { 'data-col-index': index }, column.label); if (column.tooltip) { - this._register(getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.element, column.tooltip)); + this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, column.tooltip)); } } diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index a52c00287d2..c141f381fb3 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -15,7 +15,7 @@ import 'vs/css!./toggle'; import { isActiveElement, $, addDisposableListener, EventType } from 'vs/base/browser/dom'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; export interface IToggleOpts extends IToggleStyles { @@ -113,7 +113,7 @@ export class Toggle extends Widget { readonly domNode: HTMLElement; private _checked: boolean; - private _hover: IUpdatableHover; + private _hover: IManagedHover; constructor(opts: IToggleOpts) { super(); @@ -134,7 +134,7 @@ export class Toggle extends Widget { } this.domNode = document.createElement('div'); - this._hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title)); + this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title)); this.domNode.classList.add(...classes); if (!this._opts.notFocusable) { this.domNode.tabIndex = 0; diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 57aac5edb41..ce42a2a9ac7 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -38,6 +38,16 @@ export interface IToolBarOptions { * If true, toggled primary items are highlighted with a background color. */ highlightToggledItems?: boolean; + + /** + * Render action with icons (default: `true`) + */ + icon?: boolean; + + /** + * Render action with label (default: `false`) + */ + label?: boolean; } /** @@ -50,7 +60,6 @@ export class ToolBar extends Disposable { private toggleMenuActionViewItem: DropdownMenuActionViewItem | undefined; private submenuActionViewItems: DropdownMenuActionViewItem[] = []; private hasSecondaryActions: boolean = false; - private readonly lookupKeybindings: boolean; private readonly element: HTMLElement; private _onDidChangeDropdownVisibility = this._register(new EventMultiplexer()); @@ -62,7 +71,6 @@ export class ToolBar extends Disposable { options.hoverDelegate = options.hoverDelegate ?? this._register(createInstantHoverDelegate()); this.options = options; - this.lookupKeybindings = typeof this.options.getKeyBinding === 'function'; this.toggleMenuAction = this._register(new ToggleMenuAction(() => this.toggleMenuActionViewItem?.show(), options.toggleMenuTitle)); @@ -198,7 +206,7 @@ export class ToolBar extends Disposable { } primaryActionsToSet.forEach(action => { - this.actionBar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) }); + this.actionBar.push(action, { icon: this.options.icon ?? true, label: this.options.label ?? false, keybinding: this.getKeybindingLabel(action) }); }); } @@ -207,7 +215,7 @@ export class ToolBar extends Disposable { } private getKeybindingLabel(action: IAction): string | undefined { - const key = this.lookupKeybindings ? this.options.getKeyBinding?.(action) : undefined; + const key = this.options.getKeyBinding?.(action); return key?.getLabel() ?? undefined; } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 7868ee55f6b..250efbc4296 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -796,7 +796,7 @@ class FindWidget extends Disposable { super(); container.appendChild(this.elements.root); - this._register(toDisposable(() => container.removeChild(this.elements.root))); + this._register(toDisposable(() => this.elements.root.remove())); const styles = options?.styles ?? unthemedFindWidgetStyles; diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index bfd5af68914..debb1a04685 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -569,10 +569,6 @@ export class AsyncDataTree implements IDisposable this.tree.resort(this.getDataNode(element), recursive); } - hasElement(element: TInput | T): boolean { - return this.tree.hasElement(this.getDataNode(element)); - } - hasNode(element: TInput | T): boolean { return element === this.root.element || this.nodes.has(element as T); } diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index 28402b5ffda..a1fd3249a18 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -258,14 +258,14 @@ export class EmptySubmenuAction extends Action { } } -export function toAction(props: { id: string; label: string; enabled?: boolean; checked?: boolean; class?: string; run: Function }): IAction { +export function toAction(props: { id: string; label: string; tooltip?: string; enabled?: boolean; checked?: boolean; class?: string; run: Function }): IAction { return { id: props.id, label: props.label, + tooltip: props.tooltip ?? props.label, class: props.class, enabled: props.enabled ?? true, checked: props.checked, run: async (...args: unknown[]) => props.run(...args), - tooltip: props.label }; } diff --git a/src/vs/base/common/amd.ts b/src/vs/base/common/amd.ts index 2ee86a98646..6d228840331 100644 --- a/src/vs/base/common/amd.ts +++ b/src/vs/base/common/amd.ts @@ -10,6 +10,28 @@ export const isESM = false; // export const isESM = true; // ESM-uncomment-end +export const enum LoaderEventType { + LoaderAvailable = 1, + + BeginLoadingScript = 10, + EndLoadingScriptOK = 11, + EndLoadingScriptError = 12, + + BeginInvokeFactory = 21, + EndInvokeFactory = 22, + + NodeBeginEvaluatingScript = 31, + NodeEndEvaluatingScript = 32, + + NodeBeginNativeRequire = 33, + NodeEndNativeRequire = 34, + + CachedDataFound = 60, + CachedDataMissed = 61, + CachedDataRejected = 62, + CachedDataCreated = 63, +} + export abstract class LoaderStats { abstract get amdLoad(): [string, number][]; abstract get amdInvoke(): [string, number][]; diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts index 0b306144e5e..d0df190c75b 100644 --- a/src/vs/base/common/collections.ts +++ b/src/vs/base/common/collections.ts @@ -80,3 +80,61 @@ export function intersection(setA: Set, setB: Iterable): Set { } return result; } + +export class SetWithKey implements Set { + private _map = new Map(); + + constructor(values: T[], private toKey: (t: T) => any) { + for (const value of values) { + this.add(value); + } + } + + get size(): number { + return this._map.size; + } + + add(value: T): this { + const key = this.toKey(value); + this._map.set(key, value); + return this; + } + + delete(value: T): boolean { + return this._map.delete(this.toKey(value)); + } + + has(value: T): boolean { + return this._map.has(this.toKey(value)); + } + + *entries(): IterableIterator<[T, T]> { + for (const entry of this._map.values()) { + yield [entry, entry]; + } + } + + keys(): IterableIterator { + return this.values(); + } + + *values(): IterableIterator { + for (const entry of this._map.values()) { + yield entry; + } + } + + clear(): void { + this._map.clear(); + } + + forEach(callbackfn: (value: T, value2: T, set: Set) => void, thisArg?: any): void { + this._map.forEach(entry => callbackfn.call(thisArg, entry, entry, this)); + } + + [Symbol.iterator](): IterableIterator { + return this.values(); + } + + [Symbol.toStringTag]: string = 'SetWithKey'; +} diff --git a/src/vs/base/common/equals.ts b/src/vs/base/common/equals.ts index 22825c59d9e..6e2ae8503ab 100644 --- a/src/vs/base/common/equals.ts +++ b/src/vs/base/common/equals.ts @@ -6,6 +6,10 @@ import * as arrays from 'vs/base/common/arrays'; export type EqualityComparer = (a: T, b: T) => boolean; + +/** + * Compares two items for equality using strict equality. +*/ export const strictEquals: EqualityComparer = (a, b) => a === b; /** @@ -30,11 +34,30 @@ export function itemEquals(): EqualityC return (a, b) => a.equals(b); } -export function equalsIfDefined(v1: T | undefined, v2: T | undefined, equals: EqualityComparer): boolean { - if (!v1 || !v2) { - return v1 === v2; +/** + * Checks if two items are both null or undefined, or are equal according to the provided equality comparer. +*/ +export function equalsIfDefined(v1: T | undefined | null, v2: T | undefined | null, equals: EqualityComparer): boolean; +/** + * Returns an equality comparer that checks if two items are both null or undefined, or are equal according to the provided equality comparer. +*/ +export function equalsIfDefined(equals: EqualityComparer): EqualityComparer; +export function equalsIfDefined(equalsOrV1: EqualityComparer | T, v2?: T | undefined | null, equals?: EqualityComparer): EqualityComparer | boolean { + if (equals !== undefined) { + const v1 = equalsOrV1 as T | undefined; + if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) { + return v2 === v1; + } + return equals(v1, v2); + } else { + const equals = equalsOrV1 as EqualityComparer; + return (v1, v2) => { + if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) { + return v2 === v1; + } + return equals(v1, v2); + }; } - return equals(v1, v2); } /** diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index f0d9296057b..ce5d8b29852 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -137,6 +137,19 @@ export function transformErrorForSerialization(error: any): any { return error; } +export function transformErrorFromSerialization(data: SerializedError): Error { + let error: Error; + if (data.noTelemetry) { + error = new ErrorNoTelemetry(); + } else { + error = new Error(); + error.name = data.name; + } + error.message = data.message; + error.stack = data.stack; + return error; +} + // see https://github.com/v8/v8/wiki/Stack%20Trace%20API#basic-stack-traces export interface V8CallSite { getThis(): unknown; diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index f94fa3673b6..d563a2c77db 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -831,13 +831,15 @@ export function setGlobalLeakWarningThreshold(n: number): IDisposable { class LeakageMonitor { + private static _idPool = 1; + private _stacks: Map | undefined; private _warnCountdown: number = 0; constructor( private readonly _errorHandler: (err: Error) => void, readonly threshold: number, - readonly name: string = Math.random().toString(18).slice(2, 5), + readonly name: string = (LeakageMonitor._idPool++).toString(16).padStart(3, '0') ) { } dispose(): void { @@ -952,14 +954,26 @@ const forEachListener = (listeners: ListenerOrListeners, fn: (c: ListenerC }; -const _listenerFinalizers = _enableListenerGCedWarning - ? new FinalizationRegistry(heldValue => { - if (typeof heldValue === 'string') { - console.warn('[LEAKING LISTENER] GC\'ed a listener that was NOT yet disposed. This is where is was created:'); - console.warn(heldValue); +let _listenerFinalizers: FinalizationRegistry | undefined; + +if (_enableListenerGCedWarning) { + const leaks: string[] = []; + + setInterval(() => { + if (leaks.length === 0) { + return; } - }) - : undefined; + console.warn('[LEAKING LISTENERS] GC\'ed these listeners that were NOT yet disposed:'); + console.warn(leaks.join('\n')); + leaks.length = 0; + }, 3000); + + _listenerFinalizers = new FinalizationRegistry(heldValue => { + if (typeof heldValue === 'string') { + leaks.push(heldValue); + } + }); +} /** * The Emitter can be used to expose an Event to the public @@ -1126,8 +1140,9 @@ export class Emitter { } if (_listenerFinalizers) { - const stack = new Error().stack!.split('\n').slice(2).join('\n').trim(); - _listenerFinalizers.register(result, stack, result); + const stack = new Error().stack!.split('\n').slice(2, 3).join('\n').trim(); + const match = /(file:|vscode-file:\/\/vscode-app)?(\/[^:]*:\d+:\d+)/.exec(stack); + _listenerFinalizers.register(result, match?.[2] ?? stack, result); } return result; diff --git a/src/vs/base/common/history.ts b/src/vs/base/common/history.ts index de578810355..9d644a851c5 100644 --- a/src/vs/base/common/history.ts +++ b/src/vs/base/common/history.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { SetWithKey } from 'vs/base/common/collections'; import { ArrayNavigator, INavigator } from 'vs/base/common/navigator'; export class HistoryNavigator implements INavigator { @@ -114,6 +115,10 @@ interface HistoryNode { next: HistoryNode | undefined; } +/** + * The right way to use HistoryNavigator2 is for the last item in the list to be the user's uncommitted current text. eg empty string, or whatever has been typed. Then + * the user can navigate away from the last item through the list, and back to it. When updating the last item, call replaceLast. + */ export class HistoryNavigator2 { private valueSet: Set; @@ -123,7 +128,7 @@ export class HistoryNavigator2 { private _size: number; get size(): number { return this._size; } - constructor(history: readonly T[], private capacity: number = 10) { + constructor(history: readonly T[], private capacity: number = 10, private identityFn: (t: T) => any = t => t) { if (history.length < 1) { throw new Error('not supported'); } @@ -135,7 +140,7 @@ export class HistoryNavigator2 { next: undefined }; - this.valueSet = new Set([history[0]]); + this.valueSet = new SetWithKey([history[0]], identityFn); for (let i = 1; i < history.length; i++) { this.add(history[i]); } @@ -172,7 +177,7 @@ export class HistoryNavigator2 { * @returns old last value */ replaceLast(value: T): T { - if (this.tail.value === value) { + if (this.identityFn(this.tail.value) === this.identityFn(value)) { return value; } @@ -252,8 +257,9 @@ export class HistoryNavigator2 { private _deleteFromList(value: T): void { let temp = this.head; + const valueKey = this.identityFn(value); while (temp !== this.tail) { - if (temp.value === value) { + if (this.identityFn(temp.value) === valueKey) { if (temp === this.head) { this.head = this.head.next!; this.head.previous = undefined; diff --git a/src/vs/base/common/hotReload.ts b/src/vs/base/common/hotReload.ts index 94dec8e9b2f..609fd9d8ef2 100644 --- a/src/vs/base/common/hotReload.ts +++ b/src/vs/base/common/hotReload.ts @@ -41,9 +41,23 @@ function registerGlobalHotReloadHandler() { g.$hotReload_applyNewExports = args => { const args2 = { config: { mode: undefined }, ...args }; + const results: AcceptNewExportsHandler[] = []; for (const h of hotReloadHandlers!) { const result = h(args2); - if (result) { return result; } + if (result) { + results.push(result); + } + } + if (results.length > 0) { + return newExports => { + let result = false; + for (const r of results) { + if (r(newExports)) { + result = true; + } + } + return result; + }; } return undefined; }; diff --git a/src/vs/base/common/hotReloadHelpers.ts b/src/vs/base/common/hotReloadHelpers.ts new file mode 100644 index 00000000000..174b1adcbcd --- /dev/null +++ b/src/vs/base/common/hotReloadHelpers.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isHotReloadEnabled, registerHotReloadHandler } from 'vs/base/common/hotReload'; +import { IReader, observableSignalFromEvent } from 'vs/base/common/observable'; + +export function readHotReloadableExport(value: T, reader: IReader | undefined): T { + observeHotReloadableExports([value], reader); + return value; +} + +export function observeHotReloadableExports(values: any[], reader: IReader | undefined): void { + if (isHotReloadEnabled()) { + const o = observableSignalFromEvent( + 'reload', + event => registerHotReloadHandler(({ oldExports }) => { + if (![...Object.values(oldExports)].some(v => values.includes(v))) { + return undefined; + } + return (_newExports) => { + event(undefined); + return true; + }; + }) + ); + o.read(reader); + } +} diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 0bbc2413bb5..c329ed6dc71 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -44,9 +44,10 @@ export namespace Iterable { return iterable[Symbol.iterator]().next().value; } - export function some(iterable: Iterable, predicate: (t: T) => unknown): boolean { + export function some(iterable: Iterable, predicate: (t: T, i: number) => unknown): boolean { + let i = 0; for (const element of iterable) { - if (predicate(element)) { + if (predicate(element, i++)) { return true; } } @@ -82,6 +83,13 @@ export namespace Iterable { } } + export function* flatMap(iterable: Iterable, fn: (t: T, index: number) => Iterable): Iterable { + let index = 0; + for (const element of iterable) { + yield* fn(element, index++); + } + } + export function* concat(...iterables: Iterable[]): Iterable { for (const iterable of iterables) { yield* iterable; diff --git a/src/vs/base/common/json.ts b/src/vs/base/common/json.ts index dadcbaf74f1..e4adc59003e 100644 --- a/src/vs/base/common/json.ts +++ b/src/vs/base/common/json.ts @@ -1308,40 +1308,6 @@ export function visit(text: string, visitor: JSONVisitor, options: ParseOptions return true; } -/** - * Takes JSON with JavaScript-style comments and remove - * them. Optionally replaces every none-newline character - * of comments with a replaceCharacter - */ -export function stripComments(text: string, replaceCh?: string): string { - - const _scanner = createScanner(text); - const parts: string[] = []; - let kind: SyntaxKind; - let offset = 0; - let pos: number; - - do { - pos = _scanner.getPosition(); - kind = _scanner.scan(); - switch (kind) { - case SyntaxKind.LineCommentTrivia: - case SyntaxKind.BlockCommentTrivia: - case SyntaxKind.EOF: - if (offset !== pos) { - parts.push(text.substring(offset, pos)); - } - if (replaceCh !== undefined) { - parts.push(_scanner.getTokenValue().replace(/[^\r\n]/g, replaceCh)); - } - offset = _scanner.getPosition(); - break; - } - } while (kind !== SyntaxKind.EOF); - - return parts.join(''); -} - export function getNodeType(value: any): NodeType { switch (typeof value) { case 'boolean': return 'boolean'; diff --git a/src/vs/base/common/jsonSchema.ts b/src/vs/base/common/jsonSchema.ts index 4216b0e5c0d..fbf6d9813fa 100644 --- a/src/vs/base/common/jsonSchema.ts +++ b/src/vs/base/common/jsonSchema.ts @@ -118,3 +118,150 @@ export type SchemaToType = T extends { type: 'string' } : T extends { type: 'array'; items: infer I } ? Array> : never; + +interface Equals { schemas: IJSONSchema[]; id?: string } + +export function getCompressedContent(schema: IJSONSchema): string { + let hasDups = false; + + + // visit all schema nodes and collect the ones that are equal + const equalsByString = new Map(); + const nodeToEquals = new Map(); + const visitSchemas = (next: IJSONSchema) => { + if (schema === next) { + return true; + } + const val = JSON.stringify(next); + if (val.length < 30) { + // the $ref takes around 25 chars, so we don't save anything + return true; + } + const eq = equalsByString.get(val); + if (!eq) { + const newEq = { schemas: [next] }; + equalsByString.set(val, newEq); + nodeToEquals.set(next, newEq); + return true; + } + eq.schemas.push(next); + nodeToEquals.set(next, eq); + hasDups = true; + return false; + }; + traverseNodes(schema, visitSchemas); + equalsByString.clear(); + + if (!hasDups) { + return JSON.stringify(schema); + } + + let defNodeName = '$defs'; + while (schema.hasOwnProperty(defNodeName)) { + defNodeName += '_'; + } + + // used to collect all schemas that are later put in `$defs`. The index in the array is the id of the schema. + const definitions: IJSONSchema[] = []; + + function stringify(root: IJSONSchema): string { + return JSON.stringify(root, (_key: string, value: any) => { + if (value !== root) { + const eq = nodeToEquals.get(value); + if (eq && eq.schemas.length > 1) { + if (!eq.id) { + eq.id = `_${definitions.length}`; + definitions.push(eq.schemas[0]); + } + return { $ref: `#/${defNodeName}/${eq.id}` }; + } + } + return value; + }); + } + + // stringify the schema and replace duplicate subtrees with $ref + // this will add new items to the definitions array + const str = stringify(schema); + + // now stringify the definitions. Each invication of stringify cann add new items to the definitions array, so the length can grow while we iterate + const defStrings: string[] = []; + for (let i = 0; i < definitions.length; i++) { + defStrings.push(`"_${i}":${stringify(definitions[i])}`); + } + if (defStrings.length) { + return `${str.substring(0, str.length - 1)},"${defNodeName}":{${defStrings.join(',')}}}`; + } + return str; +} + +type IJSONSchemaRef = IJSONSchema | boolean; + +function isObject(thing: any): thing is object { + return typeof thing === 'object' && thing !== null; +} + +/* + * Traverse a JSON schema and visit each schema node +*/ +function traverseNodes(root: IJSONSchema, visit: (schema: IJSONSchema) => boolean) { + if (!root || typeof root !== 'object') { + return; + } + const collectEntries = (...entries: (IJSONSchemaRef | undefined)[]) => { + for (const entry of entries) { + if (isObject(entry)) { + toWalk.push(entry); + } + } + }; + const collectMapEntries = (...maps: (IJSONSchemaMap | undefined)[]) => { + for (const map of maps) { + if (isObject(map)) { + for (const key in map) { + const entry = map[key]; + if (isObject(entry)) { + toWalk.push(entry); + } + } + } + } + }; + const collectArrayEntries = (...arrays: (IJSONSchemaRef[] | undefined)[]) => { + for (const array of arrays) { + if (Array.isArray(array)) { + for (const entry of array) { + if (isObject(entry)) { + toWalk.push(entry); + } + } + } + } + }; + const collectEntryOrArrayEntries = (items: (IJSONSchemaRef[] | IJSONSchemaRef | undefined)) => { + if (Array.isArray(items)) { + for (const entry of items) { + if (isObject(entry)) { + toWalk.push(entry); + } + } + } else if (isObject(items)) { + toWalk.push(items); + } + }; + + const toWalk: IJSONSchema[] = [root]; + + let next = toWalk.pop(); + while (next) { + const visitChildern = visit(next); + if (visitChildern) { + collectEntries(next.additionalItems, next.additionalProperties, next.not, next.contains, next.propertyNames, next.if, next.then, next.else, next.unevaluatedItems, next.unevaluatedProperties); + collectMapEntries(next.definitions, next.$defs, next.properties, next.patternProperties, next.dependencies, next.dependentSchemas); + collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.prefixItems); + collectEntryOrArrayEntries(next.items); + } + next = toWalk.pop(); + } +} + diff --git a/src/vs/base/common/stripComments.d.ts b/src/vs/base/common/jsonc.d.ts similarity index 74% rename from src/vs/base/common/stripComments.d.ts rename to src/vs/base/common/jsonc.d.ts index af5b182b5bf..504e6c60f9f 100644 --- a/src/vs/base/common/stripComments.d.ts +++ b/src/vs/base/common/jsonc.d.ts @@ -3,11 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/** + * A drop-in replacement for JSON.parse that can parse + * JSON with comments and trailing commas. + * + * @param content the content to strip comments from + * @returns the parsed content as JSON +*/ +export function parse(content: string): any; + /** * Strips single and multi line JavaScript comments from JSON * content. Ignores characters in strings BUT doesn't support * string continuation across multiple lines since it is not * supported in JSON. + * * @param content the content to strip comments from * @returns the content without comments */ diff --git a/src/vs/base/common/stripComments.js b/src/vs/base/common/jsonc.js similarity index 78% rename from src/vs/base/common/stripComments.js rename to src/vs/base/common/jsonc.js index c59205e14ab..7d8eacfdc10 100644 --- a/src/vs/base/common/stripComments.js +++ b/src/vs/base/common/jsonc.js @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; +/// //@ts-check +'use strict'; (function () { function factory(path, os, productName, cwd) { @@ -17,7 +18,6 @@ const regexp = /("[^"\\]*(?:\\.[^"\\]*)*")|('[^'\\]*(?:\\.[^'\\]*)*')|(\/\*[^\/\*]*(?:(?:\*|\/)[^\/\*]*)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))|(,\s*[}\]])/g; /** - * * @param {string} content * @returns {string} */ @@ -46,12 +46,27 @@ } }); } + + /** + * @param {string} content + * @returns {any} + */ + function parse(content) { + const commentsStripped = stripComments(content); + + try { + return JSON.parse(commentsStripped); + } catch (error) { + const trailingCommasStriped = commentsStripped.replace(/,\s*([}\]])/g, '$1'); + return JSON.parse(trailingCommasStriped); + } + } return { - stripComments + stripComments, + parse }; } - if (typeof define === 'function') { // amd define([], function () { return factory(); }); @@ -59,6 +74,6 @@ // commonjs module.exports = factory(); } else { - console.trace('strip comments defined in UNKNOWN context (neither requirejs or commonjs)'); + console.trace('jsonc defined in UNKNOWN context (neither requirejs or commonjs)'); } })(); diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index ca348c31a4d..2d5f856ba7a 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -74,8 +74,15 @@ export namespace Schemas { /** Scheme used for code blocks in chat. */ export const vscodeChatCodeBlock = 'vscode-chat-code-block'; + + /** + * Scheme used for backing documents created by copilot for chat. + */ + export const vscodeCopilotBackingChatCodeBlock = 'vscode-copilot-chat-code-block'; + /** Scheme used for LHS of code compare (aka diff) blocks in chat. */ export const vscodeChatCodeCompareBlock = 'vscode-chat-code-compare-block'; + /** Scheme used for the chat input editor. */ export const vscodeChatSesssion = 'vscode-chat-editor'; diff --git a/src/vs/base/common/numbers.ts b/src/vs/base/common/numbers.ts index 29e65b86032..ab4c9f92e06 100644 --- a/src/vs/base/common/numbers.ts +++ b/src/vs/base/common/numbers.ts @@ -69,3 +69,30 @@ export class SlidingWindowAverage { return this._val; } } + +/** Returns whether the point is within the triangle formed by the following 6 x/y point pairs */ +export function isPointWithinTriangle( + x: number, y: number, + ax: number, ay: number, + bx: number, by: number, + cx: number, cy: number +) { + const v0x = cx - ax; + const v0y = cy - ay; + const v1x = bx - ax; + const v1y = by - ay; + const v2x = x - ax; + const v2y = y - ay; + + const dot00 = v0x * v0x + v0y * v0y; + const dot01 = v0x * v1x + v0y * v1y; + const dot02 = v0x * v2x + v0y * v2y; + const dot11 = v1x * v1x + v1y * v1y; + const dot12 = v1x * v2x + v1y * v2y; + + const invDenom = 1 / (dot00 * dot11 - dot01 * dot01); + const u = (dot11 * dot02 - dot01 * dot12) * invDenom; + const v = (dot00 * dot12 - dot01 * dot02) * invDenom; + + return u >= 0 && v >= 0 && u + v < 1; +} diff --git a/src/vs/base/common/observable.ts b/src/vs/base/common/observable.ts index a4b21404a1a..c090a272068 100644 --- a/src/vs/base/common/observable.ts +++ b/src/vs/base/common/observable.ts @@ -60,6 +60,9 @@ export { waitForState, derivedWithCancellationToken, } from 'vs/base/common/observableInternal/promise'; +export { + observableValueOpts +} from 'vs/base/common/observableInternal/api'; import { ConsoleObservableLogger, setLogger } from 'vs/base/common/observableInternal/logging'; diff --git a/src/vs/base/common/observableInternal/api.ts b/src/vs/base/common/observableInternal/api.ts new file mode 100644 index 00000000000..6e56671b7e5 --- /dev/null +++ b/src/vs/base/common/observableInternal/api.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EqualityComparer, strictEquals } from 'vs/base/common/equals'; +import { ISettableObservable } from 'vs/base/common/observable'; +import { ObservableValue } from 'vs/base/common/observableInternal/base'; +import { IDebugNameData, DebugNameData } from 'vs/base/common/observableInternal/debugName'; +import { LazyObservableValue } from 'vs/base/common/observableInternal/lazyObservableValue'; + +export function observableValueOpts( + options: IDebugNameData & { + equalsFn?: EqualityComparer; + lazy?: boolean; + }, + initialValue: T +): ISettableObservable { + if (options.lazy) { + return new LazyObservableValue( + new DebugNameData(options.owner, options.debugName, undefined), + initialValue, + options.equalsFn ?? strictEquals, + ); + } + return new ObservableValue( + new DebugNameData(options.owner, options.debugName, undefined), + initialValue, + options.equalsFn ?? strictEquals, + ); +} diff --git a/src/vs/base/common/observableInternal/autorun.ts b/src/vs/base/common/observableInternal/autorun.ts index a2f169ee4d6..845e870d65d 100644 --- a/src/vs/base/common/observableInternal/autorun.ts +++ b/src/vs/base/common/observableInternal/autorun.ts @@ -76,7 +76,7 @@ export function autorunWithStoreHandleChanges( { owner: options.owner, debugName: options.debugName, - debugReferenceFn: options.debugReferenceFn, + debugReferenceFn: options.debugReferenceFn ?? fn, createEmptyChangeSummary: options.createEmptyChangeSummary, handleChange: options.handleChange, }, @@ -154,7 +154,7 @@ export class AutorunObserver implements IObserver, IReader } constructor( - private readonly _debugNameData: DebugNameData, + public readonly _debugNameData: DebugNameData, public readonly _runFn: (reader: IReader, changeSummary: TChangeSummary) => void, private readonly createChangeSummary: (() => TChangeSummary) | undefined, private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined, diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 7f76c8cc1ab..3c63a20116d 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -6,7 +6,7 @@ import { strictEquals, EqualityComparer } from 'vs/base/common/equals'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { keepObserved, recomputeInitiallyAndOnChange } from 'vs/base/common/observable'; -import { DebugNameData, IDebugNameData, Owner, getFunctionName } from 'vs/base/common/observableInternal/debugName'; +import { DebugNameData, DebugOwner, getFunctionName } from 'vs/base/common/observableInternal/debugName'; import type { derivedOpts } from 'vs/base/common/observableInternal/derived'; import { getLogger } from 'vs/base/common/observableInternal/logging'; @@ -201,9 +201,9 @@ export abstract class ConvenientObservable implements IObservable(fn: (value: T, reader: IReader) => TNew): IObservable; - public map(owner: Owner, fn: (value: T, reader: IReader) => TNew): IObservable; - public map(fnOrOwner: Owner | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew): IObservable { - const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as Owner; + public map(owner: DebugOwner, fn: (value: T, reader: IReader) => TNew): IObservable; + public map(fnOrOwner: DebugOwner | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew): IObservable { + const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as DebugOwner; const fn = fnOrUndefined === undefined ? fnOrOwner as (value: T, reader: IReader) => TNew : fnOrUndefined; return _derived( @@ -385,19 +385,6 @@ export function observableValue(nameOrOwner: string | object, return new ObservableValue(debugNameData, initialValue, strictEquals); } -export function observableValueOpts( - options: IDebugNameData & { - equalsFn?: EqualityComparer; - }, - initialValue: T -): ISettableObservable { - return new ObservableValue( - new DebugNameData(options.owner, options.debugName, undefined), - initialValue, - options.equalsFn ?? strictEquals, - ); -} - export class ObservableValue extends BaseObservable implements ISettableObservable { diff --git a/src/vs/base/common/observableInternal/debugName.ts b/src/vs/base/common/observableInternal/debugName.ts index 481d24f0377..1ff1f244357 100644 --- a/src/vs/base/common/observableInternal/debugName.ts +++ b/src/vs/base/common/observableInternal/debugName.ts @@ -8,7 +8,7 @@ export interface IDebugNameData { * The owner object of an observable. * Used for debugging only, such as computing a name for the observable by iterating over the fields of the owner. */ - readonly owner?: Owner | undefined; + readonly owner?: DebugOwner | undefined; /** * A string or function that returns a string that represents the name of the observable. @@ -25,7 +25,7 @@ export interface IDebugNameData { export class DebugNameData { constructor( - public readonly owner: Owner | undefined, + public readonly owner: DebugOwner | undefined, public readonly debugNameSource: DebugNameSource | undefined, public readonly referenceFn: Function | undefined, ) { } @@ -36,10 +36,10 @@ export class DebugNameData { } /** - * The owner object of an observable. + * The owning object of an observable. * Is only used for debugging purposes, such as computing a name for the observable by iterating over the fields of the owner. */ -export type Owner = object | undefined; +export type DebugOwner = object | undefined; export type DebugNameSource = string | (() => string | undefined); const countPerName = new Map(); diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index 9e95bf9dccc..8de22247dbf 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -7,7 +7,7 @@ import { assertFn } from 'vs/base/common/assert'; import { EqualityComparer, strictEquals } from 'vs/base/common/equals'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { BaseObservable, IChangeContext, IObservable, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from 'vs/base/common/observableInternal/base'; -import { DebugNameData, IDebugNameData, Owner } from 'vs/base/common/observableInternal/debugName'; +import { DebugNameData, IDebugNameData, DebugOwner } from 'vs/base/common/observableInternal/debugName'; import { getLogger } from 'vs/base/common/observableInternal/logging'; /** @@ -17,8 +17,8 @@ import { getLogger } from 'vs/base/common/observableInternal/logging'; * {@link computeFn} should start with a JS Doc using `@description` to name the derived. */ export function derived(computeFn: (reader: IReader) => T): IObservable; -export function derived(owner: Owner, computeFn: (reader: IReader) => T): IObservable; -export function derived(computeFnOrOwner: ((reader: IReader) => T) | Owner, computeFn?: ((reader: IReader) => T) | undefined): IObservable { +export function derived(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable; +export function derived(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFn?: ((reader: IReader) => T) | undefined): IObservable { if (computeFn !== undefined) { return new Derived( new DebugNameData(computeFnOrOwner, undefined, computeFn), @@ -39,7 +39,7 @@ export function derived(computeFnOrOwner: ((reader: IReader) => T) | Owner, c ); } -export function derivedWithSetter(owner: Owner | undefined, computeFn: (reader: IReader) => T, setter: (value: T, transaction: ITransaction | undefined) => void): ISettableObservable { +export function derivedWithSetter(owner: DebugOwner | undefined, computeFn: (reader: IReader) => T, setter: (value: T, transaction: ITransaction | undefined) => void): ISettableObservable { return new DerivedWithSetter( new DebugNameData(owner, undefined, computeFn), computeFn, @@ -105,7 +105,7 @@ export function derivedWithStore(computeFn: (reader: IReader, store: Disposab export function derivedWithStore(owner: object, computeFn: (reader: IReader, store: DisposableStore) => T): IObservable; export function derivedWithStore(computeFnOrOwner: ((reader: IReader, store: DisposableStore) => T) | object, computeFnOrUndefined?: ((reader: IReader, store: DisposableStore) => T)): IObservable { let computeFn: (reader: IReader, store: DisposableStore) => T; - let owner: Owner; + let owner: DebugOwner; if (computeFnOrUndefined === undefined) { computeFn = computeFnOrOwner as any; owner = undefined; @@ -128,10 +128,10 @@ export function derivedWithStore(computeFnOrOwner: ((reader: IReader, store: } export function derivedDisposable(computeFn: (reader: IReader) => T): IObservable; -export function derivedDisposable(owner: Owner, computeFn: (reader: IReader) => T): IObservable; -export function derivedDisposable(computeFnOrOwner: ((reader: IReader) => T) | Owner, computeFnOrUndefined?: ((reader: IReader) => T)): IObservable { +export function derivedDisposable(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable; +export function derivedDisposable(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader) => T)): IObservable { let computeFn: (reader: IReader) => T; - let owner: Owner; + let owner: DebugOwner; if (computeFnOrUndefined === undefined) { computeFn = computeFnOrOwner as any; owner = undefined; @@ -140,11 +140,15 @@ export function derivedDisposable(computeFnOr computeFn = computeFnOrUndefined as any; } - const store = new DisposableStore(); + let store: DisposableStore | undefined = undefined; return new Derived( new DebugNameData(owner, undefined, computeFn), r => { - store.clear(); + if (!store) { + store = new DisposableStore(); + } else { + store.clear(); + } const result = computeFn(r); if (result) { store.add(result); @@ -152,7 +156,12 @@ export function derivedDisposable(computeFnOr return result; }, undefined, undefined, - () => store.dispose(), + () => { + if (store) { + store.dispose(); + store = undefined; + } + }, strictEquals ); } @@ -192,7 +201,7 @@ export class Derived extends BaseObservable im } constructor( - private readonly _debugNameData: DebugNameData, + public readonly _debugNameData: DebugNameData, public readonly _computeFn: (reader: IReader, changeSummary: TChangeSummary) => T, private readonly createChangeSummary: (() => TChangeSummary) | undefined, private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined, diff --git a/src/vs/base/common/observableInternal/lazyObservableValue.ts b/src/vs/base/common/observableInternal/lazyObservableValue.ts new file mode 100644 index 00000000000..1c35f458161 --- /dev/null +++ b/src/vs/base/common/observableInternal/lazyObservableValue.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EqualityComparer } from 'vs/base/common/equals'; +import { ISettableObservable, ITransaction } from 'vs/base/common/observable'; +import { BaseObservable, IObserver, TransactionImpl } from 'vs/base/common/observableInternal/base'; +import { DebugNameData } from 'vs/base/common/observableInternal/debugName'; + +/** + * Holds off updating observers until the value is actually read. +*/ +export class LazyObservableValue + extends BaseObservable + implements ISettableObservable { + protected _value: T; + private _isUpToDate = true; + private readonly _deltas: TChange[] = []; + + get debugName() { + return this._debugNameData.getDebugName(this) ?? 'LazyObservableValue'; + } + + constructor( + private readonly _debugNameData: DebugNameData, + initialValue: T, + private readonly _equalityComparator: EqualityComparer, + ) { + super(); + this._value = initialValue; + } + + public override get(): T { + this._update(); + return this._value; + } + + private _update(): void { + if (this._isUpToDate) { + return; + } + this._isUpToDate = true; + + if (this._deltas.length > 0) { + for (const observer of this.observers) { + for (const change of this._deltas) { + observer.handleChange(this, change); + } + } + this._deltas.length = 0; + } else { + for (const observer of this.observers) { + observer.handleChange(this, undefined); + } + } + } + + private _updateCounter = 0; + + private _beginUpdate(): void { + this._updateCounter++; + if (this._updateCounter === 1) { + for (const observer of this.observers) { + observer.beginUpdate(this); + } + } + } + + private _endUpdate(): void { + this._updateCounter--; + if (this._updateCounter === 0) { + this._update(); + + // End update could change the observer list. + const observers = [...this.observers]; + for (const r of observers) { + r.endUpdate(this); + } + } + } + + public override addObserver(observer: IObserver): void { + const shouldCallBeginUpdate = !this.observers.has(observer) && this._updateCounter > 0; + super.addObserver(observer); + + if (shouldCallBeginUpdate) { + observer.beginUpdate(this); + } + } + + public override removeObserver(observer: IObserver): void { + const shouldCallEndUpdate = this.observers.has(observer) && this._updateCounter > 0; + super.removeObserver(observer); + + if (shouldCallEndUpdate) { + // Calling end update after removing the observer makes sure endUpdate cannot be called twice here. + observer.endUpdate(this); + } + } + + public set(value: T, tx: ITransaction | undefined, change: TChange): void { + if (change === undefined && this._equalityComparator(this._value, value)) { + return; + } + + let _tx: TransactionImpl | undefined; + if (!tx) { + tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`); + } + try { + this._isUpToDate = false; + this._setValue(value); + if (change !== undefined) { + this._deltas.push(change); + } + + tx.updateObserver({ + beginUpdate: () => this._beginUpdate(), + endUpdate: () => this._endUpdate(), + handleChange: (observable, change) => { }, + handlePossibleChange: (observable) => { }, + }, this); + + if (this._updateCounter > 1) { + // We already started begin/end update, so we need to manually call handlePossibleChange + for (const observer of this.observers) { + observer.handlePossibleChange(this); + } + } + + } finally { + if (_tx) { + _tx.finish(); + } + } + } + + override toString(): string { + return `${this.debugName}: ${this._value}`; + } + + protected _setValue(newValue: T): void { + this._value = newValue; + } +} diff --git a/src/vs/base/common/observableInternal/logging.ts b/src/vs/base/common/observableInternal/logging.ts index 01fcc3cdbbf..5e4712e6923 100644 --- a/src/vs/base/common/observableInternal/logging.ts +++ b/src/vs/base/common/observableInternal/logging.ts @@ -114,7 +114,7 @@ export class ConsoleObservableLogger implements IObservableLogger { styled(derived.debugName, { color: 'BlueViolet' }), ...this.formatInfo(info), this.formatChanges(changedObservables), - { data: [{ fn: derived._computeFn }] } + { data: [{ fn: derived._debugNameData.referenceFn ?? derived._computeFn }] } ])); changedObservables.clear(); } @@ -143,7 +143,7 @@ export class ConsoleObservableLogger implements IObservableLogger { formatKind('autorun'), styled(autorun.debugName, { color: 'BlueViolet' }), this.formatChanges(changedObservables), - { data: [{ fn: autorun._runFn }] } + { data: [{ fn: autorun._debugNameData.referenceFn ?? autorun._runFn }] } ])); changedObservables.clear(); this.indentation++; diff --git a/src/vs/base/common/observableInternal/promise.ts b/src/vs/base/common/observableInternal/promise.ts index e0109a3941b..80d269c16bd 100644 --- a/src/vs/base/common/observableInternal/promise.ts +++ b/src/vs/base/common/observableInternal/promise.ts @@ -6,7 +6,7 @@ import { autorun } from 'vs/base/common/observableInternal/autorun'; import { IObservable, IReader, observableValue, transaction } from './base'; import { Derived, derived } from 'vs/base/common/observableInternal/derived'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { DebugNameData, Owner } from 'vs/base/common/observableInternal/debugName'; +import { DebugNameData, DebugOwner } from 'vs/base/common/observableInternal/debugName'; import { strictEquals } from 'vs/base/common/equals'; import { CancellationError } from 'vs/base/common/errors'; @@ -40,6 +40,10 @@ export class ObservableLazy { * A promise whose state is observable. */ export class ObservablePromise { + public static fromFn(fn: () => Promise): ObservablePromise { + return new ObservablePromise(fn()); + } + private readonly _value = observableValue | undefined>(this, undefined); /** @@ -179,7 +183,7 @@ export function derivedWithCancellationToken(computeFn: (reader: IReader, can export function derivedWithCancellationToken(owner: object, computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable; export function derivedWithCancellationToken(computeFnOrOwner: ((reader: IReader, cancellationToken: CancellationToken) => T) | object, computeFnOrUndefined?: ((reader: IReader, cancellationToken: CancellationToken) => T)): IObservable { let computeFn: (reader: IReader, store: CancellationToken) => T; - let owner: Owner; + let owner: DebugOwner; if (computeFnOrUndefined === undefined) { computeFn = computeFnOrOwner as any; owner = undefined; diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index 409fe4a10ec..1d012de3d54 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/src/vs/base/common/observableInternal/utils.ts @@ -5,12 +5,14 @@ import { Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { autorun } from 'vs/base/common/observableInternal/autorun'; +import { autorun, autorunOpts } from 'vs/base/common/observableInternal/autorun'; import { BaseObservable, ConvenientObservable, IObservable, IObserver, IReader, ITransaction, _setKeepObserved, _setRecomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from 'vs/base/common/observableInternal/base'; -import { DebugNameData, Owner, getFunctionName } from 'vs/base/common/observableInternal/debugName'; +import { DebugNameData, IDebugNameData, DebugOwner, getDebugName, } from 'vs/base/common/observableInternal/debugName'; import { derived, derivedOpts } from 'vs/base/common/observableInternal/derived'; import { getLogger } from 'vs/base/common/observableInternal/logging'; import { IValueWithChangeEvent } from '../event'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { EqualityComparer, strictEquals } from 'vs/base/common/equals'; /** * Represents an efficient observable whose value never changes. @@ -52,11 +54,49 @@ export function observableFromPromise(promise: Promise): IObservable<{ val return observable; } + +export function observableFromEvent( + owner: DebugOwner, + event: Event, + getValue: (args: TArgs | undefined) => T, +): IObservable; export function observableFromEvent( event: Event, - getValue: (args: TArgs | undefined) => T + getValue: (args: TArgs | undefined) => T, +): IObservable; +export function observableFromEvent(...args: + [owner: DebugOwner, event: Event, getValue: (args: any | undefined) => any] + | [event: Event, getValue: (args: any | undefined) => any] +): IObservable { + let owner; + let event; + let getValue; + if (args.length === 3) { + [owner, event, getValue] = args; + } else { + [event, getValue] = args; + } + return new FromEventObservable( + new DebugNameData(owner, undefined, getValue), + event, + getValue, + () => FromEventObservable.globalTransaction, + strictEquals + ); +} + +export function observableFromEventOpts( + options: IDebugNameData & { + equalsFn?: EqualityComparer; + }, + event: Event, + getValue: (args: TArgs | undefined) => T, ): IObservable { - return new FromEventObservable(event, getValue); + return new FromEventObservable( + new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? getValue), + event, + getValue, () => FromEventObservable.globalTransaction, options.equalsFn ?? strictEquals + ); } export class FromEventObservable extends BaseObservable { @@ -67,14 +107,17 @@ export class FromEventObservable extends BaseObservable { private subscription: IDisposable | undefined; constructor( + private readonly _debugNameData: DebugNameData, private readonly event: Event, - public readonly _getValue: (args: TArgs | undefined) => T + public readonly _getValue: (args: TArgs | undefined) => T, + private readonly _getTransaction: () => ITransaction | undefined, + private readonly _equalityComparator: EqualityComparer ) { super(); } private getDebugName(): string | undefined { - return getFunctionName(this._getValue); + return this._debugNameData.getDebugName(this); } public get debugName(): string { @@ -90,7 +133,7 @@ export class FromEventObservable extends BaseObservable { const newValue = this._getValue(args); const oldValue = this.value; - const didChange = !this.hasValue || oldValue !== newValue; + const didChange = !this.hasValue || !(this._equalityComparator(oldValue!, newValue)); let didRunTransaction = false; if (didChange) { @@ -99,7 +142,7 @@ export class FromEventObservable extends BaseObservable { if (this.hasValue) { didRunTransaction = true; subtransaction( - FromEventObservable.globalTransaction, + this._getTransaction(), (tx) => { getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue }); @@ -229,6 +272,10 @@ class ObservableSignal extends BaseObservable implements return new DebugNameData(this._owner, this._debugName, undefined).getDebugName(this) ?? 'Observable Signal'; } + public override toString(): string { + return this.debugName; + } + constructor( private readonly _debugName: string | undefined, private readonly _owner?: object, @@ -406,9 +453,9 @@ export class KeepAliveObserver implements IObserver { } } -export function derivedObservableWithCache(owner: Owner, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable { +export function derivedObservableWithCache(owner: DebugOwner, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable { let lastValue: T | undefined = undefined; - const observable = derived(owner, reader => { + const observable = derivedOpts({ owner, debugReferenceFn: computeFn }, reader => { lastValue = computeFn(reader, lastValue); return lastValue; }); @@ -439,7 +486,7 @@ export function derivedObservableWithWritableCache(owner: object, computeFn: /** * When the items array changes, referential equal items are not mapped again. */ -export function mapObservableArrayCached(owner: Owner, items: IObservable, map: (input: TIn, store: DisposableStore) => TOut, keySelector?: (input: TIn) => TKey): IObservable { +export function mapObservableArrayCached(owner: DebugOwner, items: IObservable, map: (input: TIn, store: DisposableStore) => TOut, keySelector?: (input: TIn) => TKey): IObservable { let m = new ArrayMap(map, keySelector); const self = derivedOpts({ debugReferenceFn: map, @@ -515,9 +562,49 @@ export class ValueWithChangeEventFromObservable implements IValueWithChangeEv } } -export function observableFromValueWithChangeEvent(_owner: Owner, value: IValueWithChangeEvent): IObservable { +export function observableFromValueWithChangeEvent(owner: DebugOwner, value: IValueWithChangeEvent): IObservable { if (value instanceof ValueWithChangeEventFromObservable) { return value.observable; } - return observableFromEvent(value.onDidChange, () => value.value); + return observableFromEvent(owner, value.onDidChange, () => value.value); +} + +/** + * Creates an observable that has the latest changed value of the given observables. + * Initially (and when not observed), it has the value of the last observable. + * When observed and any of the observables change, it has the value of the last changed observable. + * If multiple observables change in the same transaction, the last observable wins. +*/ +export function latestChangedValue[]>(owner: DebugOwner, observables: T): IObservable> { + if (observables.length === 0) { + throw new BugIndicatingError(); + } + + let hasLastChangedValue = false; + let lastChangedValue: any = undefined; + + const result = observableFromEvent(owner, cb => { + const store = new DisposableStore(); + for (const o of observables) { + store.add(autorunOpts({ debugName: () => getDebugName(result, new DebugNameData(owner, undefined, undefined)) + '.updateLastChangedValue' }, reader => { + hasLastChangedValue = true; + lastChangedValue = o.read(reader); + cb(); + })); + } + store.add({ + dispose() { + hasLastChangedValue = false; + lastChangedValue = undefined; + }, + }); + return store; + }, () => { + if (hasLastChangedValue) { + return lastChangedValue; + } else { + return observables[observables.length - 1].get(); + } + }); + return result; } diff --git a/src/vs/base/common/path.ts b/src/vs/base/common/path.ts index 83140948bba..6f40f7d0356 100644 --- a/src/vs/base/common/path.ts +++ b/src/vs/base/common/path.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ // NOTE: VSCode's copy of nodejs path library to be usable in common (non-node) namespace -// Copied from: https://github.com/nodejs/node/blob/v16.14.2/lib/path.js +// Copied from: https://github.com/nodejs/node/commits/v20.9.0/lib/path.js +// Excluding: the change that adds primordials +// (https://github.com/nodejs/node/commit/187a862d221dec42fa9a5c4214e7034d9092792f and others) /** * Copyright Joyent, Inc. and other Node contributors. @@ -159,11 +161,15 @@ function normalizeString(path: string, allowAboveRoot: boolean, separator: strin return res; } +function formatExt(ext: string): string { + return ext ? `${ext[0] === '.' ? '' : '.'}${ext}` : ''; +} + function _format(sep: string, pathObject: ParsedPath) { validateObject(pathObject, 'pathObject'); const dir = pathObject.dir || pathObject.root; const base = pathObject.base || - `${pathObject.name || ''}${pathObject.ext || ''}`; + `${pathObject.name || ''}${formatExt(pathObject.ext)}`; if (!dir) { return base; } @@ -185,7 +191,7 @@ export interface IPath { resolve(...pathSegments: string[]): string; relative(from: string, to: string): string; dirname(path: string): string; - basename(path: string, ext?: string): string; + basename(path: string, suffix?: string): string; extname(path: string): string; format(pathObject: ParsedPath): string; parse(path: string): ParsedPath; @@ -207,7 +213,7 @@ export const win32: IPath = { let path; if (i >= 0) { path = pathSegments[i]; - validateString(path, 'path'); + validateString(path, `paths[${i}]`); // Skip empty entries if (path.length === 0) { @@ -757,9 +763,9 @@ export const win32: IPath = { return path.slice(0, end); }, - basename(path: string, ext?: string): string { - if (ext !== undefined) { - validateString(ext, 'ext'); + basename(path: string, suffix?: string): string { + if (suffix !== undefined) { + validateString(suffix, 'suffix'); } validateString(path, 'path'); let start = 0; @@ -776,11 +782,11 @@ export const win32: IPath = { start = 2; } - if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { - if (ext === path) { + if (suffix !== undefined && suffix.length > 0 && suffix.length <= path.length) { + if (suffix === path) { return ''; } - let extIdx = ext.length - 1; + let extIdx = suffix.length - 1; let firstNonSlashEnd = -1; for (i = path.length - 1; i >= start; --i) { const code = path.charCodeAt(i); @@ -800,7 +806,7 @@ export const win32: IPath = { } if (extIdx >= 0) { // Try to match the explicit extension - if (code === ext.charCodeAt(extIdx)) { + if (code === suffix.charCodeAt(extIdx)) { if (--extIdx === -1) { // We matched the extension, so mark this as the end of our path // component @@ -1095,7 +1101,7 @@ export const posix: IPath = { for (let i = pathSegments.length - 1; i >= -1 && !resolvedAbsolute; i--) { const path = i >= 0 ? pathSegments[i] : posixCwd(); - validateString(path, 'path'); + validateString(path, `paths[${i}]`); // Skip empty entries if (path.length === 0) { @@ -1280,9 +1286,9 @@ export const posix: IPath = { return path.slice(0, end); }, - basename(path: string, ext?: string): string { - if (ext !== undefined) { - validateString(ext, 'ext'); + basename(path: string, suffix?: string): string { + if (suffix !== undefined) { + validateString(suffix, 'ext'); } validateString(path, 'path'); @@ -1291,11 +1297,11 @@ export const posix: IPath = { let matchedSlash = true; let i; - if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { - if (ext === path) { + if (suffix !== undefined && suffix.length > 0 && suffix.length <= path.length) { + if (suffix === path) { return ''; } - let extIdx = ext.length - 1; + let extIdx = suffix.length - 1; let firstNonSlashEnd = -1; for (i = path.length - 1; i >= 0; --i) { const code = path.charCodeAt(i); @@ -1315,7 +1321,7 @@ export const posix: IPath = { } if (extIdx >= 0) { // Try to match the explicit extension - if (code === ext.charCodeAt(extIdx)) { + if (code === suffix.charCodeAt(extIdx)) { if (--extIdx === -1) { // We matched the extension, so mark this as the end of our path // component diff --git a/src/vs/base/common/performance.js b/src/vs/base/common/performance.js index aff4d0734de..2af54743f33 100644 --- a/src/vs/base/common/performance.js +++ b/src/vs/base/common/performance.js @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - //@ts-check +'use strict'; (function () { @@ -42,6 +41,7 @@ // Identify browser environment when following property is not present // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#performancenodetiming + // @ts-ignore if (typeof performance === 'object' && typeof performance.mark === 'function' && !performance.nodeTiming) { // in a browser context, reuse performance-util @@ -119,6 +119,7 @@ module.exports = _factory(sharedObj); } else { console.trace('perf-util defined in UNKNOWN context (neither requirejs or commonjs)'); + // @ts-ignore sharedObj.perf = _factory(sharedObj); } diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index 2251c7db5ba..d931e64dfa9 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as nls from 'vs/nls'; export const LANGUAGE_DEFAULT = 'en'; @@ -22,13 +23,6 @@ let _platformLocale: string = LANGUAGE_DEFAULT; let _translationsConfigFile: string | undefined = undefined; let _userAgent: string | undefined = undefined; -interface NLSConfig { - locale: string; - osLocale: string; - availableLanguages: { [key: string]: string }; - _translationsConfigFile: string; -} - export interface IProcessEnvironment { [key: string]: string | undefined; } @@ -89,13 +83,11 @@ if (typeof nodeProcess === 'object') { const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG']; if (rawNlsConfig) { try { - const nlsConfig: NLSConfig = JSON.parse(rawNlsConfig); - const resolved = nlsConfig.availableLanguages['*']; - _locale = nlsConfig.locale; + const nlsConfig: nls.INLSConfiguration = JSON.parse(rawNlsConfig); + _locale = nlsConfig.userLocale; _platformLocale = nlsConfig.osLocale; - // VSCode's default language is 'en' - _language = resolved ? resolved : LANGUAGE_DEFAULT; - _translationsConfigFile = nlsConfig._translationsConfigFile; + _language = nlsConfig.resolvedLanguage || LANGUAGE_DEFAULT; + _translationsConfigFile = nlsConfig.languagePack?.translationsConfigFile; } catch (e) { } } @@ -111,18 +103,10 @@ else if (typeof navigator === 'object' && !isElectronRenderer) { _isLinux = _userAgent.indexOf('Linux') >= 0; _isMobile = _userAgent?.indexOf('Mobi') >= 0; _isWeb = true; - - const configuredLocale = nls.getConfiguredDefaultLocale( - // This call _must_ be done in the file that calls `nls.getConfiguredDefaultLocale` - // to ensure that the NLS AMD Loader plugin has been loaded and configured. - // This is because the loader plugin decides what the default locale is based on - // how it's able to resolve the strings. - nls.localize({ key: 'ensureLoaderPluginIsLoaded', comment: ['{Locked}'] }, '_') - ); - - _locale = configuredLocale || LANGUAGE_DEFAULT; - _language = _locale; - _platformLocale = navigator.language; + // VSCODE_GLOBALS: NLS + _language = globalThis._VSCODE_NLS_LANGUAGE || LANGUAGE_DEFAULT; + _locale = navigator.language.toLowerCase(); + _platformLocale = _locale; } // Unknown environment @@ -178,7 +162,7 @@ export const userAgent = _userAgent; /** * The language used for the user interface. The format of * the string is all lower case (e.g. zh-tw for Traditional - * Chinese) + * Chinese or de for German) */ export const language = _language; @@ -204,15 +188,16 @@ export namespace Language { } /** - * The OS locale or the locale specified by --locale. The format of - * the string is all lower case (e.g. zh-tw for Traditional - * Chinese). The UI is not necessarily shown in the provided locale. + * Desktop: The OS locale or the locale specified by --locale or `argv.json`. + * Web: matches `platformLocale`. + * + * The UI is not necessarily shown in the provided locale. */ export const locale = _locale; /** * This will always be set to the OS/browser's locale regardless of - * what was specified by --locale. The format of the string is all + * what was specified otherwise. The format of the string is all * lower case (e.g. zh-tw for Traditional Chinese). The UI is not * necessarily shown in the provided locale. */ diff --git a/src/vs/base/common/prefixTree.ts b/src/vs/base/common/prefixTree.ts index 8e839e2b4ff..53f02964d36 100644 --- a/src/vs/base/common/prefixTree.ts +++ b/src/vs/base/common/prefixTree.ts @@ -32,6 +32,11 @@ export class WellDefinedPrefixTree { return this.root.children?.values() || Iterable.empty(); } + /** Gets the top-level nodes of the tree */ + public get entries(): Iterable<[string, IPrefixTreeNode]> { + return this.root.children?.entries() || Iterable.empty(); + } + /** * Inserts a new value in the prefix tree. * @param onNode - called for each node as we descend to the insertion point, diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts index 417c4ba1168..ef29387bc0a 100644 --- a/src/vs/base/common/processes.ts +++ b/src/vs/base/common/processes.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IProcessEnvironment, isLinux, isMacintosh } from 'vs/base/common/platform'; +import { IProcessEnvironment, isLinux } from 'vs/base/common/platform'; /** * Options to be passed to the external program or shell. @@ -140,13 +140,6 @@ export function removeDangerousEnvVariables(env: IProcessEnvironment | undefined // See https://github.com/microsoft/vscode/issues/130072 delete env['DEBUG']; - if (isMacintosh) { - // Unset `DYLD_LIBRARY_PATH`, as it leads to process crashes - // See https://github.com/microsoft/vscode/issues/104525 - // See https://github.com/microsoft/vscode/issues/105848 - delete env['DYLD_LIBRARY_PATH']; - } - if (isLinux) { // Unset `LD_PRELOAD`, as it might lead to process crashes // See https://github.com/microsoft/vscode/issues/134177 diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index c6e335aa3e7..754eba49584 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -82,6 +82,7 @@ export interface IProductConfiguration { readonly webEndpointUrlTemplate?: string; readonly webviewContentExternalBaseUrlTemplate?: string; readonly target?: string; + readonly nlsCoreBaseUrl?: string; readonly settingsSearchBuildId?: number; readonly settingsSearchUrl?: string; @@ -173,6 +174,7 @@ export interface IProductConfiguration { readonly extensionPointExtensionKind?: { readonly [extensionPointId: string]: ('ui' | 'workspace' | 'web')[] }; readonly extensionSyncedKeys?: { readonly [extensionId: string]: string[] }; + readonly extensionsEnabledWithApiProposalVersion?: string[]; readonly extensionEnabledApiProposals?: { readonly [extensionId: string]: string[] }; readonly extensionUntrustedWorkspaceSupport?: { readonly [extensionId: string]: ExtensionUntrustedWorkspaceSupport }; readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: ExtensionVirtualWorkspaceSupport }; diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index 4d7e51431cb..f12b04da9d7 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -413,6 +413,10 @@ export class URI implements UriComponents { return result; } } + + [Symbol.for('debug.description')]() { + return `URI(${this.toString()})`; + } } export interface UriComponents { diff --git a/src/vs/base/node/extpath.ts b/src/vs/base/node/extpath.ts index a7ec9cf6d36..eb91392e390 100644 --- a/src/vs/base/node/extpath.ts +++ b/src/vs/base/node/extpath.ts @@ -119,7 +119,7 @@ export async function realpath(path: string): Promise { // to not resolve links but to simply see if the path is read accessible or not. const normalizedPath = normalizePath(path); - await Promises.access(normalizedPath, fs.constants.R_OK); + await fs.promises.access(normalizedPath, fs.constants.R_OK); return normalizedPath; } diff --git a/src/vs/base/node/languagePacks.d.ts b/src/vs/base/node/languagePacks.d.ts deleted file mode 100644 index 5dd1b1f1ee9..00000000000 --- a/src/vs/base/node/languagePacks.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export interface NLSConfiguration { - locale: string; - osLocale: string; - availableLanguages: { - [key: string]: string; - }; - pseudo?: boolean; - _languagePackSupport?: boolean; -} - -export interface InternalNLSConfiguration extends NLSConfiguration { - _languagePackId: string; - _translationsConfigFile: string; - _cacheRoot: string; - _resolvedLanguagePackCoreLocation: string; - _corruptedFile: string; - _languagePackSupport?: boolean; -} - -export function getNLSConfiguration(commit: string | undefined, userDataPath: string, metaDataFile: string, locale: string, osLocale: string): Promise; diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js deleted file mode 100644 index 0d3d1acf6a3..00000000000 --- a/src/vs/base/node/languagePacks.js +++ /dev/null @@ -1,264 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/// - -//@ts-check -(function () { - 'use strict'; - - /** - * @param {typeof import('path')} path - * @param {typeof import('fs')} fs - * @param {typeof import('../common/performance')} perf - */ - function factory(path, fs, perf) { - - /** - * @param {string} file - * @returns {Promise} - */ - function exists(file) { - return new Promise(c => fs.exists(file, c)); - } - - /** - * @param {string} file - * @returns {Promise} - */ - function touch(file) { - return new Promise((c, e) => { const d = new Date(); fs.utimes(file, d, d, err => err ? e(err) : c()); }); - } - - /** - * @param {string} dir - * @returns {Promise} - */ - function mkdirp(dir) { - return new Promise((c, e) => fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? e(err) : c(dir))); - } - - /** - * @param {string} location - * @returns {Promise} - */ - function rimraf(location) { - return new Promise((c, e) => fs.rm(location, { recursive: true, force: true, maxRetries: 3 }, err => err ? e(err) : c())); - } - - /** - * @param {string} file - * @returns {Promise} - */ - function readFile(file) { - return new Promise((c, e) => fs.readFile(file, 'utf8', (err, data) => err ? e(err) : c(data))); - } - - /** - * @param {string} file - * @param {string} content - * @returns {Promise} - */ - function writeFile(file, content) { - return new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c())); - } - - /** - * @param {string} userDataPath - * @returns {Promise} - */ - async function getLanguagePackConfigurations(userDataPath) { - const configFile = path.join(userDataPath, 'languagepacks.json'); - try { - return JSON.parse(await readFile(configFile)); - } catch (err) { - // Do nothing. If we can't read the file we have no - // language pack config. - } - return undefined; - } - - /** - * @param {object} config - * @param {string | undefined} locale - */ - function resolveLanguagePackLocale(config, locale) { - try { - while (locale) { - if (config[locale]) { - return locale; - } else { - const index = locale.lastIndexOf('-'); - if (index > 0) { - locale = locale.substring(0, index); - } else { - return undefined; - } - } - } - } catch (err) { - console.error('Resolving language pack configuration failed.', err); - } - return undefined; - } - - /** - * @param {string | undefined} commit - * @param {string} userDataPath - * @param {string} metaDataFile - * @param {string} locale - * @param {string} osLocale - * @returns {Promise} - */ - function getNLSConfiguration(commit, userDataPath, metaDataFile, locale, osLocale) { - const defaultResult = function (locale) { - perf.mark('code/didGenerateNls'); - return Promise.resolve({ locale, osLocale, availableLanguages: {} }); - }; - - perf.mark('code/willGenerateNls'); - - if (locale === 'pseudo') { - return Promise.resolve({ locale, osLocale, availableLanguages: {}, pseudo: true }); - } - - if (process.env['VSCODE_DEV']) { - return Promise.resolve({ locale, osLocale, availableLanguages: {} }); - } - - // We have a built version so we have extracted nls file. Try to find - // the right file to use. - - // Check if we have an English or English US locale. If so fall to default since that is our - // English translation (we don't ship *.nls.en.json files) - if (locale && (locale === 'en' || locale === 'en-us')) { - return Promise.resolve({ locale, osLocale, availableLanguages: {} }); - } - - const initialLocale = locale; - - try { - if (!commit) { - return defaultResult(initialLocale); - } - return getLanguagePackConfigurations(userDataPath).then(configs => { - if (!configs) { - return defaultResult(initialLocale); - } - const resolvedLocale = resolveLanguagePackLocale(configs, locale); - if (!resolvedLocale) { - return defaultResult(initialLocale); - } - locale = resolvedLocale; - const packConfig = configs[locale]; - let mainPack; - if (!packConfig || typeof packConfig.hash !== 'string' || !packConfig.translations || typeof (mainPack = packConfig.translations['vscode']) !== 'string') { - return defaultResult(initialLocale); - } - return exists(mainPack).then(fileExists => { - if (!fileExists) { - return defaultResult(initialLocale); - } - const packId = packConfig.hash + '.' + locale; - const cacheRoot = path.join(userDataPath, 'clp', packId); - const coreLocation = path.join(cacheRoot, commit); - const translationsConfigFile = path.join(cacheRoot, 'tcf.json'); - const corruptedFile = path.join(cacheRoot, 'corrupted.info'); - const result = { - locale: initialLocale, - osLocale, - availableLanguages: { '*': locale }, - _languagePackId: packId, - _translationsConfigFile: translationsConfigFile, - _cacheRoot: cacheRoot, - _resolvedLanguagePackCoreLocation: coreLocation, - _corruptedFile: corruptedFile - }; - return exists(corruptedFile).then(corrupted => { - // The nls cache directory is corrupted. - let toDelete; - if (corrupted) { - toDelete = rimraf(cacheRoot); - } else { - toDelete = Promise.resolve(undefined); - } - return toDelete.then(() => { - return exists(coreLocation).then(fileExists => { - if (fileExists) { - // We don't wait for this. No big harm if we can't touch - touch(coreLocation).catch(() => { }); - perf.mark('code/didGenerateNls'); - return result; - } - return mkdirp(coreLocation).then(() => { - return Promise.all([readFile(metaDataFile), readFile(mainPack)]); - }).then(values => { - const metadata = JSON.parse(values[0]); - const packData = JSON.parse(values[1]).contents; - const bundles = Object.keys(metadata.bundles); - const writes = []; - for (const bundle of bundles) { - const modules = metadata.bundles[bundle]; - const target = Object.create(null); - for (const module of modules) { - const keys = metadata.keys[module]; - const defaultMessages = metadata.messages[module]; - const translations = packData[module]; - let targetStrings; - if (translations) { - targetStrings = []; - for (let i = 0; i < keys.length; i++) { - const elem = keys[i]; - const key = typeof elem === 'string' ? elem : elem.key; - let translatedMessage = translations[key]; - if (translatedMessage === undefined) { - translatedMessage = defaultMessages[i]; - } - targetStrings.push(translatedMessage); - } - } else { - targetStrings = defaultMessages; - } - target[module] = targetStrings; - } - writes.push(writeFile(path.join(coreLocation, bundle.replace(/\//g, '!') + '.nls.json'), JSON.stringify(target))); - } - writes.push(writeFile(translationsConfigFile, JSON.stringify(packConfig.translations))); - return Promise.all(writes); - }).then(() => { - perf.mark('code/didGenerateNls'); - return result; - }).catch(err => { - console.error('Generating translation files failed.', err); - return defaultResult(locale); - }); - }); - }); - }); - }); - }); - } catch (err) { - console.error('Generating translation files failed.', err); - return defaultResult(locale); - } - } - - return { - getNLSConfiguration - }; - } - - if (typeof define === 'function') { - // amd - define(['path', 'fs', 'vs/base/common/performance'], function (/** @type {typeof import('path')} */ path, /** @type {typeof import('fs')} */ fs, /** @type {typeof import('../common/performance')} */ perf) { return factory(path, fs, perf); }); - } else if (typeof module === 'object' && typeof module.exports === 'object') { - const path = require('path'); - const fs = require('fs'); - const perf = require('../common/performance'); - module.exports = factory(path, fs, perf); - } else { - throw new Error('Unknown context'); - } -}()); diff --git a/src/vs/base/node/nls.d.ts b/src/vs/base/node/nls.d.ts new file mode 100644 index 00000000000..94ef503e4e3 --- /dev/null +++ b/src/vs/base/node/nls.d.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { INLSConfiguration } from 'vs/nls'; + +export interface IResolveNLSConfigurationContext { + + /** + * Location where `nls.messages.json` and `nls.keys.json` are stored. + */ + readonly nlsMetadataPath: string; + + /** + * Path to the user data directory. Used as a cache for + * language packs converted to the format we need. + */ + readonly userDataPath: string; + + /** + * Commit of the running application. Can be `undefined` + * when not built. + */ + readonly commit: string | undefined; + + /** + * Locale as defined in `argv.json` or `app.getLocale()`. + */ + readonly userLocale: string; + + /** + * Locale as defined by the OS (e.g. `app.getPreferredSystemLanguages()`). + */ + readonly osLocale: string; +} + +export function resolveNLSConfiguration(context: IResolveNLSConfigurationContext): Promise; diff --git a/src/vs/base/node/nls.js b/src/vs/base/node/nls.js new file mode 100644 index 00000000000..c171eb6542e --- /dev/null +++ b/src/vs/base/node/nls.js @@ -0,0 +1,259 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// + +//@ts-check +'use strict'; + +/** + * @import { INLSConfiguration, ILanguagePacks } from '../../nls' + * @import { IResolveNLSConfigurationContext } from './nls' + */ + +(function () { + + /** + * @param {typeof import('path')} path + * @param {typeof import('fs')} fs + * @param {typeof import('../common/performance')} perf + */ + function factory(path, fs, perf) { + + //#region fs helpers + + /** + * @param {string} path + */ + async function exists(path) { + try { + await fs.promises.access(path); + + return true; + } catch { + return false; + } + } + + /** + * @param {string} path + */ + function touch(path) { + const date = new Date(); + return fs.promises.utimes(path, date, date); + } + + //#endregion + + /** + * The `languagepacks.json` file is a JSON file that contains all metadata + * about installed language extensions per language. Specifically, for + * core (`vscode`) and all extensions it supports, it points to the related + * translation files. + * + * The file is updated whenever a new language pack is installed or removed. + * + * @param {string} userDataPath + * @returns {Promise} + */ + async function getLanguagePackConfigurations(userDataPath) { + const configFile = path.join(userDataPath, 'languagepacks.json'); + try { + return JSON.parse(await fs.promises.readFile(configFile, 'utf-8')); + } catch (err) { + return undefined; // Do nothing. If we can't read the file we have no language pack config. + } + } + + /** + * @param {ILanguagePacks} languagePacks + * @param {string | undefined} locale + */ + function resolveLanguagePackLanguage(languagePacks, locale) { + try { + while (locale) { + if (languagePacks[locale]) { + return locale; + } + + const index = locale.lastIndexOf('-'); + if (index > 0) { + locale = locale.substring(0, index); + } else { + return undefined; + } + } + } catch (error) { + console.error('Resolving language pack configuration failed.', error); + } + + return undefined; + } + + /** + * @param {string} userLocale + * @param {string} osLocale + * @param {string} nlsMetadataPath + * @returns {INLSConfiguration} + */ + function defaultNLSConfiguration(userLocale, osLocale, nlsMetadataPath) { + perf.mark('code/didGenerateNls'); + + return { + userLocale, + osLocale, + resolvedLanguage: 'en', + defaultMessagesFile: path.join(nlsMetadataPath, 'nls.messages.json'), + + // NLS: below 2 are a relic from old times only used by vscode-nls and deprecated + locale: 'en', + availableLanguages: {} + }; + } + + /** + * @param {IResolveNLSConfigurationContext} context + * @returns {Promise} + */ + async function resolveNLSConfiguration({ userLocale, osLocale, userDataPath, commit, nlsMetadataPath }) { + perf.mark('code/willGenerateNls'); + + if ( + process.env['VSCODE_DEV'] || + userLocale === 'pseudo' || + userLocale.startsWith('en') || + !commit || + !userDataPath + ) { + return defaultNLSConfiguration(userLocale, osLocale, nlsMetadataPath); + } + + try { + const languagePacks = await getLanguagePackConfigurations(userDataPath); + if (!languagePacks) { + return defaultNLSConfiguration(userLocale, osLocale, nlsMetadataPath); + } + + const resolvedLanguage = resolveLanguagePackLanguage(languagePacks, userLocale); + if (!resolvedLanguage) { + return defaultNLSConfiguration(userLocale, osLocale, nlsMetadataPath); + } + + const languagePack = languagePacks[resolvedLanguage]; + const mainLanguagePackPath = languagePack?.translations?.['vscode']; + if ( + !languagePack || + typeof languagePack.hash !== 'string' || + !languagePack.translations || + typeof mainLanguagePackPath !== 'string' || + !(await exists(mainLanguagePackPath)) + ) { + return defaultNLSConfiguration(userLocale, osLocale, nlsMetadataPath); + } + + const languagePackId = `${languagePack.hash}.${resolvedLanguage}`; + const globalLanguagePackCachePath = path.join(userDataPath, 'clp', languagePackId); + const commitLanguagePackCachePath = path.join(globalLanguagePackCachePath, commit); + const languagePackMessagesFile = path.join(commitLanguagePackCachePath, 'nls.messages.json'); + const translationsConfigFile = path.join(globalLanguagePackCachePath, 'tcf.json'); + const languagePackCorruptMarkerFile = path.join(globalLanguagePackCachePath, 'corrupted.info'); + + if (await exists(languagePackCorruptMarkerFile)) { + await fs.promises.rm(globalLanguagePackCachePath, { recursive: true, force: true, maxRetries: 3 }); // delete corrupted cache folder + } + + /** @type {INLSConfiguration} */ + const result = { + userLocale, + osLocale, + resolvedLanguage, + defaultMessagesFile: path.join(nlsMetadataPath, 'nls.messages.json'), + languagePack: { + translationsConfigFile, + messagesFile: languagePackMessagesFile, + corruptMarkerFile: languagePackCorruptMarkerFile + }, + + // NLS: below properties are a relic from old times only used by vscode-nls and deprecated + locale: userLocale, + availableLanguages: { '*': resolvedLanguage }, + _languagePackId: languagePackId, + _languagePackSupport: true, + _translationsConfigFile: translationsConfigFile, + _cacheRoot: globalLanguagePackCachePath, + _resolvedLanguagePackCoreLocation: commitLanguagePackCachePath, + _corruptedFile: languagePackCorruptMarkerFile + }; + + if (await exists(commitLanguagePackCachePath)) { + touch(commitLanguagePackCachePath).catch(() => { }); // We don't wait for this. No big harm if we can't touch + perf.mark('code/didGenerateNls'); + return result; + } + + /** @type {[unknown, Array<[string, string[]]>, string[], { contents: Record> }]} */ + // ^moduleId ^nlsKeys ^moduleId ^nlsKey ^nlsValue + const [ + , + nlsDefaultKeys, + nlsDefaultMessages, + nlsPackdata + ] = await Promise.all([ + fs.promises.mkdir(commitLanguagePackCachePath, { recursive: true }), + JSON.parse(await fs.promises.readFile(path.join(nlsMetadataPath, 'nls.keys.json'), 'utf-8')), + JSON.parse(await fs.promises.readFile(path.join(nlsMetadataPath, 'nls.messages.json'), 'utf-8')), + JSON.parse(await fs.promises.readFile(mainLanguagePackPath, 'utf-8')) + ]); + + /** @type {string[]} */ + const nlsResult = []; + + // We expect NLS messages to be in a flat array in sorted order as they + // where produced during build time. We use `nls.keys.json` to know the + // right order and then lookup the related message from the translation. + // If a translation does not exist, we fallback to the default message. + + let nlsIndex = 0; + for (const [moduleId, nlsKeys] of nlsDefaultKeys) { + const moduleTranslations = nlsPackdata.contents[moduleId]; + for (const nlsKey of nlsKeys) { + nlsResult.push(moduleTranslations?.[nlsKey] || nlsDefaultMessages[nlsIndex]); + nlsIndex++; + } + } + + await Promise.all([ + fs.promises.writeFile(languagePackMessagesFile, JSON.stringify(nlsResult), 'utf-8'), + fs.promises.writeFile(translationsConfigFile, JSON.stringify(languagePack.translations), 'utf-8') + ]); + + perf.mark('code/didGenerateNls'); + + return result; + } catch (error) { + console.error('Generating translation files failed.', error); + } + + return defaultNLSConfiguration(userLocale, osLocale, nlsMetadataPath); + } + + return { + resolveNLSConfiguration + }; + } + + if (typeof define === 'function') { + // amd + define(['path', 'fs', 'vs/base/common/performance'], function (/** @type {typeof import('path')} */ path, /** @type {typeof import('fs')} */ fs, /** @type {typeof import('../common/performance')} */ perf) { return factory(path, fs, perf); }); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + // commonjs + const path = require('path'); + const fs = require('fs'); + const perf = require('../common/performance'); + module.exports = factory(path, fs, perf); + } else { + throw new Error('vs/base/node/nls defined in UNKNOWN context (neither requirejs or commonjs)'); + } +})(); diff --git a/src/vs/base/node/osDisplayProtocolInfo.ts b/src/vs/base/node/osDisplayProtocolInfo.ts index c028dc88536..90c825bd29b 100644 --- a/src/vs/base/node/osDisplayProtocolInfo.ts +++ b/src/vs/base/node/osDisplayProtocolInfo.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { constants as FSConstants } from 'fs'; -import { access } from 'fs/promises'; +import { constants as FSConstants, promises as FSPromises } from 'fs'; import { join } from 'vs/base/common/path'; import { env } from 'vs/base/common/process'; @@ -44,7 +43,7 @@ export async function getDisplayProtocol(errorLogger: (error: any) => void): Pro const waylandServerPipe = join(xdgRuntimeDir, 'wayland-0'); try { - await access(waylandServerPipe, FSConstants.R_OK); + await FSPromises.access(waylandServerPipe, FSConstants.R_OK); // If the file exists, then the session is wayland. return DisplayProtocolType.Wayland; diff --git a/src/vs/base/node/osReleaseInfo.ts b/src/vs/base/node/osReleaseInfo.ts index f72b0fe82ba..60101f545cb 100644 --- a/src/vs/base/node/osReleaseInfo.ts +++ b/src/vs/base/node/osReleaseInfo.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { constants as FSConstants } from 'fs'; -import { open, FileHandle } from 'fs/promises'; +import { constants as FSConstants, promises as FSPromises } from 'fs'; import { createInterface as readLines } from 'readline'; import * as Platform from 'vs/base/common/platform'; @@ -22,10 +21,10 @@ export async function getOSReleaseInfo(errorLogger: (error: any) => void): Promi // Extract release information on linux based systems // using the identifiers specified in // https://www.freedesktop.org/software/systemd/man/os-release.html - let handle: FileHandle | undefined; + let handle: FSPromises.FileHandle | undefined; for (const filePath of ['/etc/os-release', '/usr/lib/os-release', '/etc/lsb-release']) { try { - handle = await open(filePath, FSConstants.R_OK); + handle = await FSPromises.open(filePath, FSConstants.R_OK); break; } catch (err) { } } diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 025df98b423..52e8c9e87d4 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -60,14 +60,6 @@ async function rimraf(path: string, mode = RimRafMode.UNLINK, moveToPath?: strin async function rimrafMove(path: string, moveToPath = randomPath(tmpdir())): Promise { try { try { - // Intentionally using `fs.promises` here to skip - // the patched graceful-fs method that can result - // in very long running `rename` calls when the - // folder is locked by a file watcher. We do not - // really want to slow down this operation more - // than necessary and we have a fallback to delete - // via unlink. - // https://github.com/microsoft/vscode/issues/139908 await fs.promises.rename(path, moveToPath); } catch (error) { if (error.code === 'ENOENT') { @@ -87,7 +79,7 @@ async function rimrafMove(path: string, moveToPath = randomPath(tmpdir())): Prom } async function rimrafUnlink(path: string): Promise { - return promisify(fs.rm)(path, { recursive: true, force: true, maxRetries: 3 }); + return fs.promises.rm(path, { recursive: true, force: true, maxRetries: 3 }); } export function rimrafSync(path: string): void { @@ -118,12 +110,12 @@ export interface IDirent { async function readdir(path: string): Promise; async function readdir(path: string, options: { withFileTypes: true }): Promise; async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> { - return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : promisify(fs.readdir)(path))); + return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : fs.promises.readdir(path))); } async function safeReaddirWithFileTypes(path: string): Promise { try { - return await promisify(fs.readdir)(path, { withFileTypes: true }); + return await fs.promises.readdir(path, { withFileTypes: true }); } catch (error) { console.warn('[node.js fs] readdir with filetypes failed with error: ', error); } @@ -142,7 +134,7 @@ async function safeReaddirWithFileTypes(path: string): Promise { let isSymbolicLink = false; try { - const lstat = await Promises.lstat(join(path, child)); + const lstat = await fs.promises.lstat(join(path, child)); isFile = lstat.isFile(); isDirectory = lstat.isDirectory(); @@ -267,7 +259,7 @@ export namespace SymlinkSupport { // First stat the link let lstats: fs.Stats | undefined; try { - lstats = await Promises.lstat(path); + lstats = await fs.promises.lstat(path); // Return early if the stat is not a symbolic link at all if (!lstats.isSymbolicLink()) { @@ -280,7 +272,7 @@ export namespace SymlinkSupport { // If the stat is a symbolic link or failed to stat, use fs.stat() // which for symbolic links will stat the target they point to try { - const stats = await Promises.stat(path); + const stats = await fs.promises.stat(path); return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined }; } catch (error) { @@ -295,7 +287,7 @@ export namespace SymlinkSupport { // are not supported (https://github.com/nodejs/node/issues/36790) if (isWindows && error.code === 'EACCES') { try { - const stats = await Promises.stat(await Promises.readlink(path)); + const stats = await fs.promises.stat(await fs.promises.readlink(path)); return { stat: stats, symbolicLink: { dangling: false } }; } catch (error) { @@ -493,7 +485,7 @@ function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptio * - allows to move across multiple disks * - attempts to retry the operation for certain error codes on Windows */ -async function rename(source: string, target: string, windowsRetryTimeout: number | false = 60000 /* matches graceful-fs */): Promise { +async function rename(source: string, target: string, windowsRetryTimeout: number | false = 60000): Promise { if (source === target) { return; // simulate node.js behaviour here and do a no-op if paths match } @@ -501,12 +493,10 @@ async function rename(source: string, target: string, windowsRetryTimeout: numbe try { if (isWindows && typeof windowsRetryTimeout === 'number') { // On Windows, a rename can fail when either source or target - // is locked by AV software. We do leverage graceful-fs to iron - // out these issues, however in case the target file exists, - // graceful-fs will immediately return without retry for fs.rename(). + // is locked by AV software. await renameWithRetry(source, target, Date.now(), windowsRetryTimeout); } else { - await promisify(fs.rename)(source, target); + await fs.promises.rename(source, target); } } catch (error) { // In two cases we fallback to classic copy and delete: @@ -528,7 +518,7 @@ async function rename(source: string, target: string, windowsRetryTimeout: numbe async function renameWithRetry(source: string, target: string, startTime: number, retryTimeout: number, attempt = 0): Promise { try { - return await promisify(fs.rename)(source, target); + return await fs.promises.rename(source, target); } catch (error) { if (error.code !== 'EACCES' && error.code !== 'EPERM' && error.code !== 'EBUSY') { throw error; // only for errors we think are temporary @@ -630,7 +620,7 @@ async function doCopy(source: string, target: string, payload: ICopyPayload): Pr async function doCopyDirectory(source: string, target: string, mode: number, payload: ICopyPayload): Promise { // Create folder - await Promises.mkdir(target, { recursive: true, mode }); + await fs.promises.mkdir(target, { recursive: true, mode }); // Copy each file recursively const files = await readdir(source); @@ -642,16 +632,16 @@ async function doCopyDirectory(source: string, target: string, mode: number, pay async function doCopyFile(source: string, target: string, mode: number): Promise { // Copy file - await Promises.copyFile(source, target); + await fs.promises.copyFile(source, target); // restore mode (https://github.com/nodejs/node/issues/1104) - await Promises.chmod(target, mode); + await fs.promises.chmod(target, mode); } async function doCopySymlink(source: string, target: string, payload: ICopyPayload): Promise { // Figure out link target - let linkTarget = await Promises.readlink(source); + let linkTarget = await fs.promises.readlink(source); // Special case: the symlink points to a target that is // actually within the path that is being copied. In that @@ -662,7 +652,7 @@ async function doCopySymlink(source: string, target: string, payload: ICopyPaylo } // Create symlink - await Promises.symlink(linkTarget, target); + await fs.promises.symlink(linkTarget, target); } //#endregion @@ -670,31 +660,19 @@ async function doCopySymlink(source: string, target: string, payload: ICopyPaylo //#region Promise based fs methods /** - * Prefer this helper class over the `fs.promises` API to - * enable `graceful-fs` to function properly. Given issue - * https://github.com/isaacs/node-graceful-fs/issues/160 it - * is evident that the module only takes care of the non-promise - * based fs methods. + * Some low level `fs` methods provided as `Promises` similar to + * `fs.promises` but with notable differences, either implemented + * by us or by restoring the original callback based behavior. * - * Another reason is `realpath` being entirely different in - * the promise based implementation compared to the other - * one (https://github.com/microsoft/vscode/issues/118562) - * - * Note: using getters for a reason, since `graceful-fs` - * patching might kick in later after modules have been - * loaded we need to defer access to fs methods. - * (https://github.com/microsoft/vscode/issues/124176) + * At least `realpath` is implemented differently in the promise + * based implementation compared to the callback based one. The + * promise based implementation actually calls `fs.realpath.native`. + * (https://github.com/microsoft/vscode/issues/118562) */ export const Promises = new class { //#region Implemented by node.js - get access() { return promisify(fs.access); } - - get stat() { return promisify(fs.stat); } - get lstat() { return promisify(fs.lstat); } - get utimes() { return promisify(fs.utimes); } - get read() { // Not using `promisify` here for a reason: the return @@ -713,7 +691,6 @@ export const Promises = new class { }); }; } - get readFile() { return promisify(fs.readFile); } get write() { @@ -734,27 +711,12 @@ export const Promises = new class { }; } - get appendFile() { return promisify(fs.appendFile); } + get fdatasync() { return promisify(fs.fdatasync); } // not exposed as API in 20.x yet - get fdatasync() { return promisify(fs.fdatasync); } - get truncate() { return promisify(fs.truncate); } + get open() { return promisify(fs.open); } // changed to return `FileHandle` in promise API + get close() { return promisify(fs.close); } // not exposed as API due to the `FileHandle` return type of `open` - get copyFile() { return promisify(fs.copyFile); } - - get open() { return promisify(fs.open); } - get close() { return promisify(fs.close); } - - get symlink() { return promisify(fs.symlink); } - get readlink() { return promisify(fs.readlink); } - - get chmod() { return promisify(fs.chmod); } - - get mkdir() { return promisify(fs.mkdir); } - - get unlink() { return promisify(fs.unlink); } - get rmdir() { return promisify(fs.rmdir); } - - get realpath() { return promisify(fs.realpath); } + get realpath() { return promisify(fs.realpath); } // `fs.promises.realpath` will use `fs.realpath.native` which we do not want //#endregion @@ -762,7 +724,7 @@ export const Promises = new class { async exists(path: string): Promise { try { - await Promises.access(path); + await fs.promises.access(path); return true; } catch { diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index 9c07f106711..7bd20f80c07 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as cp from 'child_process'; -import { Stats } from 'fs'; +import { Stats, promises } from 'fs'; import * as path from 'vs/base/common/path'; import * as Platform from 'vs/base/common/platform'; import * as process from 'vs/base/common/process'; @@ -91,11 +91,11 @@ export namespace win32 { if (await pfs.Promises.exists(path)) { let statValue: Stats | undefined; try { - statValue = await pfs.Promises.stat(path); + statValue = await promises.stat(path); } catch (e) { if (e.message.startsWith('EACCES')) { // it might be symlink - statValue = await pfs.Promises.lstat(path); + statValue = await promises.lstat(path); } } return statValue ? !statValue.isDirectory() : false; diff --git a/src/vs/base/node/unc.js b/src/vs/base/node/unc.js index b0af4d38b68..e019e5258ce 100644 --- a/src/vs/base/node/unc.js +++ b/src/vs/base/node/unc.js @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; +/// //@ts-check +'use strict'; (function () { function factory() { @@ -18,6 +19,7 @@ // The property `process.uncHostAllowlist` is not available in official node.js // releases, only in our own builds, so we have to probe for availability + // @ts-ignore return process.uncHostAllowlist; } @@ -114,6 +116,7 @@ return; } + // @ts-ignore process.restrictUNCAccess = false; } @@ -122,6 +125,7 @@ return true; } + // @ts-ignore return process.restrictUNCAccess === false; } diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index c0d9b4b8ecb..1295944efcf 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createWriteStream, WriteStream } from 'fs'; +import { createWriteStream, WriteStream, promises } from 'fs'; import { Readable } from 'stream'; import { createCancelablePromise, Sequencer } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -85,7 +85,7 @@ function extractEntry(stream: Readable, fileName: string, mode: number, targetPa istream?.destroy(); }); - return Promise.resolve(Promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise((c, e) => { + return Promise.resolve(promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise((c, e) => { if (token.isCancellationRequested) { return; } @@ -148,7 +148,7 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok // directory file names end with '/' if (/\/$/.test(fileName)) { const targetFileName = path.join(targetPath, fileName); - last = createCancelablePromise(token => Promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e)); + last = createCancelablePromise(token => promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e)); return; } diff --git a/src/vs/base/parts/ipc/common/ipc.net.ts b/src/vs/base/parts/ipc/common/ipc.net.ts index 1fe8ee07800..39ba4f16236 100644 --- a/src/vs/base/parts/ipc/common/ipc.net.ts +++ b/src/vs/base/parts/ipc/common/ipc.net.ts @@ -597,6 +597,8 @@ export class Client extends IPCClient { override dispose(): void { super.dispose(); const socket = this.protocol.getSocket(); + // should be sent gracefully with a .flush(), but try to send it out as a + // last resort here if nothing else: this.protocol.sendDisconnect(); this.protocol.dispose(); socket.end(); @@ -808,6 +810,7 @@ export interface PersistentProtocolOptions { export class PersistentProtocol implements IMessagePassingProtocol { private _isReconnecting: boolean; + private _didSendDisconnect?: boolean; private _outgoingUnackMsg: Queue; private _outgoingMsgId: number; @@ -910,9 +913,12 @@ export class PersistentProtocol implements IMessagePassingProtocol { } sendDisconnect(): void { - const msg = new ProtocolMessage(ProtocolMessageType.Disconnect, 0, 0, getEmptyBuffer()); - this._socketWriter.write(msg); - this._socketWriter.flush(); + if (!this._didSendDisconnect) { + this._didSendDisconnect = true; + const msg = new ProtocolMessage(ProtocolMessageType.Disconnect, 0, 0, getEmptyBuffer()); + this._socketWriter.write(msg); + this._socketWriter.flush(); + } } sendPause(): void { diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 6530fac0d7b..f1bed382c62 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -426,18 +426,18 @@ export class ChannelServer implements IChannelServer { - this.sendResponse({ id, data, type: ResponseType.PromiseSuccess }); + this.sendResponse({ id, data, type: ResponseType.PromiseSuccess }); }, err => { if (err instanceof Error) { - this.sendResponse({ + this.sendResponse({ id, data: { message: err.message, name: err.name, - stack: err.stack ? (err.stack.split ? err.stack.split('\n') : err.stack) : undefined + stack: err.stack ? err.stack.split('\n') : undefined }, type: ResponseType.PromiseError }); } else { - this.sendResponse({ id, data: err, type: ResponseType.PromiseErrorObj }); + this.sendResponse({ id, data: err, type: ResponseType.PromiseErrorObj }); } }).finally(() => { disposable.dispose(); @@ -458,7 +458,7 @@ export class ChannelServer implements IChannelServer this.sendResponse({ id, data, type: ResponseType.EventFire })); + const disposable = event(data => this.sendResponse({ id, data, type: ResponseType.EventFire })); this.activeRequests.set(request.id, disposable); } @@ -484,7 +484,7 @@ export class ChannelServer implements IChannelServer{ + this.sendResponse({ id: request.id, data: { name: 'Unknown channel', message: `Channel name '${request.channelName}' timed out after ${this.timeoutDelay}ms`, stack: undefined }, type: ResponseType.PromiseError diff --git a/src/vs/base/parts/ipc/electron-main/ipcMain.ts b/src/vs/base/parts/ipc/electron-main/ipcMain.ts index c72b6e8e644..a0bf9a1603f 100644 --- a/src/vs/base/parts/ipc/electron-main/ipcMain.ts +++ b/src/vs/base/parts/ipc/electron-main/ipcMain.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ipcMain as unsafeIpcMain, IpcMainEvent, IpcMainInvokeEvent } from 'electron'; +import electron from 'electron'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { VSCODE_AUTHORITY } from 'vs/base/common/network'; -type ipcMainListener = (event: IpcMainEvent, ...args: any[]) => void; +type ipcMainListener = (event: electron.IpcMainEvent, ...args: any[]) => void; class ValidatedIpcMain implements Event.NodeEventEmitter { @@ -25,7 +25,7 @@ class ValidatedIpcMain implements Event.NodeEventEmitter { // Remember the wrapped listener so that later we can // properly implement `removeListener`. - const wrappedListener = (event: IpcMainEvent, ...args: any[]) => { + const wrappedListener = (event: electron.IpcMainEvent, ...args: any[]) => { if (this.validateEvent(channel, event)) { listener(event, ...args); } @@ -33,7 +33,7 @@ class ValidatedIpcMain implements Event.NodeEventEmitter { this.mapListenerToWrapper.set(listener, wrappedListener); - unsafeIpcMain.on(channel, wrappedListener); + electron.ipcMain.on(channel, wrappedListener); return this; } @@ -43,7 +43,7 @@ class ValidatedIpcMain implements Event.NodeEventEmitter { * only the next time a message is sent to `channel`, after which it is removed. */ once(channel: string, listener: ipcMainListener): this { - unsafeIpcMain.once(channel, (event: IpcMainEvent, ...args: any[]) => { + electron.ipcMain.once(channel, (event: electron.IpcMainEvent, ...args: any[]) => { if (this.validateEvent(channel, event)) { listener(event, ...args); } @@ -68,8 +68,8 @@ class ValidatedIpcMain implements Event.NodeEventEmitter { * are serialized and only the `message` property from the original error is * provided to the renderer process. Please refer to #24427 for details. */ - handle(channel: string, listener: (event: IpcMainInvokeEvent, ...args: any[]) => Promise): this { - unsafeIpcMain.handle(channel, (event: IpcMainInvokeEvent, ...args: any[]) => { + handle(channel: string, listener: (event: electron.IpcMainInvokeEvent, ...args: any[]) => Promise): this { + electron.ipcMain.handle(channel, (event: electron.IpcMainInvokeEvent, ...args: any[]) => { if (this.validateEvent(channel, event)) { return listener(event, ...args); } @@ -84,7 +84,7 @@ class ValidatedIpcMain implements Event.NodeEventEmitter { * Removes any handler for `channel`, if present. */ removeHandler(channel: string): this { - unsafeIpcMain.removeHandler(channel); + electron.ipcMain.removeHandler(channel); return this; } @@ -96,14 +96,14 @@ class ValidatedIpcMain implements Event.NodeEventEmitter { removeListener(channel: string, listener: ipcMainListener): this { const wrappedListener = this.mapListenerToWrapper.get(listener); if (wrappedListener) { - unsafeIpcMain.removeListener(channel, wrappedListener); + electron.ipcMain.removeListener(channel, wrappedListener); this.mapListenerToWrapper.delete(listener); } return this; } - private validateEvent(channel: string, event: IpcMainEvent | IpcMainInvokeEvent): boolean { + private validateEvent(channel: string, event: electron.IpcMainEvent | electron.IpcMainInvokeEvent): boolean { if (!channel || !channel.startsWith('vscode:')) { onUnexpectedError(`Refused to handle ipcMain event for channel '${channel}' because the channel is unknown.`); return false; // unexpected channel diff --git a/src/vs/base/parts/ipc/node/ipc.net.ts b/src/vs/base/parts/ipc/node/ipc.net.ts index 7d57ca6cb47..0f3dd812b6d 100644 --- a/src/vs/base/parts/ipc/node/ipc.net.ts +++ b/src/vs/base/parts/ipc/node/ipc.net.ts @@ -17,6 +17,15 @@ import { generateUuid } from 'vs/base/common/uuid'; import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc'; import { ChunkStream, Client, ISocket, Protocol, SocketCloseEvent, SocketCloseEventType, SocketDiagnostics, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net'; +/** + * Maximum time to wait for a 'close' event to fire after the socket stream + * ends. For unix domain sockets, the close event may not fire consistently + * due to what appears to be a Node.js bug. + * + * @see https://github.com/microsoft/vscode/issues/211462#issuecomment-2155471996 + */ +const socketEndTimeoutMs = 30_000; + export class NodeSocket implements ISocket { public readonly debugLabel: string; @@ -51,15 +60,20 @@ export class NodeSocket implements ISocket { }; this.socket.on('error', this._errorListener); + let endTimeoutHandle: NodeJS.Timeout | undefined; this._closeListener = (hadError: boolean) => { this.traceSocketEvent(SocketDiagnosticsEventType.Close, { hadError }); this._canWrite = false; + if (endTimeoutHandle) { + clearTimeout(endTimeoutHandle); + } }; this.socket.on('close', this._closeListener); this._endListener = () => { this.traceSocketEvent(SocketDiagnosticsEventType.NodeEndReceived); this._canWrite = false; + endTimeoutHandle = setTimeout(() => socket.destroy(), socketEndTimeoutMs); }; this.socket.on('end', this._endListener); } diff --git a/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts b/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts index 00813be686f..c49d9bf8855 100644 --- a/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts +++ b/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { Client as MessagePortClient } from 'vs/base/parts/ipc/browser/ipc.mp'; diff --git a/src/vs/base/parts/ipc/test/common/ipc.test.ts b/src/vs/base/parts/ipc/test/common/ipc.test.ts index 0515d24a49c..b9fc881a222 100644 --- a/src/vs/base/parts/ipc/test/common/ipc.test.ts +++ b/src/vs/base/parts/ipc/test/common/ipc.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; diff --git a/src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts b/src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts index 44559562cea..4bb851ebffe 100644 --- a/src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts +++ b/src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Client as MessagePortClient } from 'vs/base/parts/ipc/browser/ipc.mp'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts b/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts index d761f720de7..9caacd5b537 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.cp.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; diff --git a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts index 0e018300cea..bac8dc992f6 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts @@ -3,14 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; +import sinon from 'sinon'; import { EventEmitter } from 'events'; import { AddressInfo, connect, createServer, Server, Socket } from 'net'; import { tmpdir } from 'os'; import { Barrier, timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { ILoadEstimator, PersistentProtocol, Protocol, ProtocolConstants, SocketCloseEvent, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net'; import { createRandomIPCHandle, createStaticIPCHandle, NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { flakySuite } from 'vs/base/test/common/testUtils'; @@ -134,7 +135,7 @@ class Ether { suite('IPC, Socket Protocol', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const ds = ensureNoDisposablesAreLeakedInTestSuite(); let ether: Ether; @@ -186,6 +187,26 @@ suite('IPC, Socket Protocol', () => { b.dispose(); }); + + + test('issue #211462: destroy socket after end timeout', async () => { + const socket = new EventEmitter(); + Object.assign(socket, { destroy: () => socket.emit('close') }); + const protocol = ds.add(new Protocol(new NodeSocket(socket as Socket))); + + const disposed = sinon.stub(); + const timers = sinon.useFakeTimers(); + + ds.add(toDisposable(() => timers.restore())); + ds.add(protocol.onDidDispose(disposed)); + + socket.emit('end'); + assert.ok(!disposed.called); + timers.tick(29_999); + assert.ok(!disposed.called); + timers.tick(1); + assert.ok(disposed.called); + }); }); suite('PersistentProtocol reconnection', () => { diff --git a/src/vs/base/parts/sandbox/common/sandboxTypes.ts b/src/vs/base/parts/sandbox/common/sandboxTypes.ts index 8c296184f7d..9b607da8103 100644 --- a/src/vs/base/parts/sandbox/common/sandboxTypes.ts +++ b/src/vs/base/parts/sandbox/common/sandboxTypes.ts @@ -53,4 +53,21 @@ export interface ISandboxConfiguration { * Location of V8 code cache. */ codeCachePath?: string; + + /** + * NLS support + */ + nls: { + + /** + * All NLS messages produced by `localize` and `localize2` calls + * under `src/vs`. + */ + messages: string[]; + + /** + * The actual language of the NLS messages (e.g. 'en', de' or 'pt-br'). + */ + language: string | undefined; + }; } diff --git a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts index ba8ea6446a6..a132d7d6eb4 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts @@ -172,3 +172,19 @@ export interface AuthInfo { port: number; realm: string; } + +export interface WebUtils { + + // Docs: https://electronjs.org/docs/api/web-utils + + /** + * The file system path that this `File` object points to. In the case where the + * object passed in is not a `File` object an exception is thrown. In the case + * where the File object passed in was constructed in JS and is not backed by a + * file on disk an empty string is returned. + * + * This method superceded the previous augmentation to the `File` object with the + * `path` property. An example is included below. + */ + getPathForFile(file: File): string; +} diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 9a01b61c669..10737a23101 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -5,18 +5,13 @@ import { INodeProcess, IProcessEnvironment } from 'vs/base/common/platform'; import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes'; -import { IpcRenderer, ProcessMemoryInfo, WebFrame } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; +import { IpcRenderer, ProcessMemoryInfo, WebFrame, WebUtils } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; /** * In Electron renderers we cannot expose all of the `process` global of node.js */ export interface ISandboxNodeProcess extends INodeProcess { - /** - * The process.pid property returns the process ID of the process. - */ - readonly pid: number; - /** * The process.platform property returns a string identifying the operating system platform * on which the Node.js process is running. @@ -126,6 +121,7 @@ export const ipcMessagePort: IpcMessagePort = vscodeGlobal.ipcMessagePort; export const webFrame: WebFrame = vscodeGlobal.webFrame; export const process: ISandboxNodeProcess = vscodeGlobal.process; export const context: ISandboxContext = vscodeGlobal.context; +export const webUtils: WebUtils = vscodeGlobal.webUtils; /** * A set of globals that are available in all windows that either diff --git a/src/vs/base/parts/sandbox/electron-sandbox/preload.js b/src/vs/base/parts/sandbox/electron-sandbox/preload.js index 4c51b45d18b..7e2339e49da 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/preload.js +++ b/src/vs/base/parts/sandbox/electron-sandbox/preload.js @@ -7,7 +7,14 @@ (function () { 'use strict'; - const { ipcRenderer, webFrame, contextBridge } = require('electron'); + /** + * @import { ISandboxConfiguration } from '../common/sandboxTypes' + * @import { IpcRenderer } from './electronTypes' + * @import { IpcRendererEvent } from 'electron' + * @import { ISandboxNodeProcess } from './globals' + */ + + const { ipcRenderer, webFrame, contextBridge, webUtils } = require('electron'); //#region Utilities @@ -41,10 +48,6 @@ //#region Resolve Configuration - /** - * @typedef {import('../common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - */ - /** @type {ISandboxConfiguration | undefined} */ let configuration = undefined; @@ -123,9 +126,6 @@ * A minimal set of methods exposed from Electron's `ipcRenderer` * to support communication to main process. * - * @typedef {import('./electronTypes').IpcRenderer} IpcRenderer - * @typedef {import('electron').IpcRendererEvent} IpcRendererEvent - * * @type {IpcRenderer} */ @@ -237,18 +237,28 @@ } }, + /** + * Support for subset of Electron's `webUtils` type. + */ + webUtils: { + + /** + * @param {File} file + */ + getPathForFile(file) { + return webUtils.getPathForFile(file); + } + }, + /** * Support for a subset of access to node.js global `process`. * * Note: when `sandbox` is enabled, the only properties available * are https://github.com/electron/electron/blob/master/docs/api/process.md#sandbox * - * @typedef {import('./globals').ISandboxNodeProcess} ISandboxNodeProcess - * * @type {ISandboxNodeProcess} */ process: { - get pid() { return process.pid; }, get platform() { return process.platform; }, get arch() { return process.arch; }, get env() { return { ...process.env }; }, diff --git a/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts b/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts index 491f2209c6d..7724c4ed465 100644 --- a/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts +++ b/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { context, ipcRenderer, process, webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import assert from 'assert'; +import { context, ipcRenderer, process, webFrame, webUtils } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Sandbox', () => { @@ -13,6 +13,7 @@ suite('Sandbox', () => { assert.ok(typeof ipcRenderer.send === 'function'); assert.ok(typeof webFrame.setZoomLevel === 'function'); assert.ok(typeof process.platform === 'string'); + assert.ok(typeof webUtils.getPathForFile === 'function'); const config = await context.resolveConfiguration(); assert.ok(config); diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index 5d942007580..33a8498c7bb 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { mapToString, setToString } from 'vs/base/common/map'; @@ -194,7 +195,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // Delete the existing DB. If the path does not exist or fails to // be deleted, we do not try to recover anymore because we assume // that the path is no longer writeable for us. - return Promises.unlink(this.path).then(() => { + return fs.promises.unlink(this.path).then(() => { // Re-open the DB fresh return this.doConnect(this.path).then(recoveryConnection => { @@ -280,7 +281,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // folder is really not writeable for us. // try { - await Promises.unlink(path); + await fs.promises.unlink(path); try { await Promises.rename(this.toBackupPath(path), path, false /* no retry */); } catch (error) { diff --git a/src/vs/base/parts/storage/test/node/storage.integrationTest.ts b/src/vs/base/parts/storage/test/node/storage.integrationTest.ts index 51c66ca1af8..450687ab705 100644 --- a/src/vs/base/parts/storage/test/node/storage.integrationTest.ts +++ b/src/vs/base/parts/storage/test/node/storage.integrationTest.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { deepStrictEqual, ok, strictEqual } from 'assert'; import { tmpdir } from 'os'; import { timeout } from 'vs/base/common/async'; @@ -24,7 +25,7 @@ flakySuite('Storage Library', function () { setup(function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); - return Promises.mkdir(testDir, { recursive: true }); + return fs.promises.mkdir(testDir, { recursive: true }); }); teardown(function () { @@ -353,7 +354,7 @@ flakySuite('SQLite Storage Library', function () { setup(function () { testdir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); - return Promises.mkdir(testdir, { recursive: true }); + return fs.promises.mkdir(testdir, { recursive: true }); }); teardown(function () { @@ -534,7 +535,7 @@ flakySuite('SQLite Storage Library', function () { // on shutdown. await storage.checkIntegrity(true).then(null, error => { } /* error is expected here but we do not want to fail */); - await Promises.unlink(backupPath); // also test that the recovery DB is backed up properly + await fs.promises.unlink(backupPath); // also test that the recovery DB is backed up properly let recoveryCalled = false; await storage.close(() => { @@ -798,7 +799,7 @@ flakySuite('SQLite Storage Library', function () { await storage.optimize(); await storage.close(); - const sizeBeforeDeleteAndOptimize = (await Promises.stat(dbPath)).size; + const sizeBeforeDeleteAndOptimize = (await fs.promises.stat(dbPath)).size; storage = new SQLiteStorageDatabase(dbPath); @@ -820,7 +821,7 @@ flakySuite('SQLite Storage Library', function () { await storage.close(); - const sizeAfterDeleteAndOptimize = (await Promises.stat(dbPath)).size; + const sizeAfterDeleteAndOptimize = (await fs.promises.stat(dbPath)).size; strictEqual(sizeAfterDeleteAndOptimize < sizeBeforeDeleteAndOptimize, true); }); diff --git a/src/vs/base/test/browser/actionbar.test.ts b/src/vs/base/test/browser/actionbar.test.ts index fc119f36de9..5d197e8181b 100644 --- a/src/vs/base/test/browser/actionbar.test.ts +++ b/src/vs/base/test/browser/actionbar.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ActionBar, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, Separator } from 'vs/base/common/actions'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/browser.test.ts b/src/vs/base/test/browser/browser.test.ts index da58ddd6a91..76049fb5721 100644 --- a/src/vs/base/test/browser/browser.test.ts +++ b/src/vs/base/test/browser/browser.test.ts @@ -2,10 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('Browsers', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + test('all', () => { assert(!(isWindows && isMacintosh)); }); diff --git a/src/vs/base/test/browser/comparers.test.ts b/src/vs/base/test/browser/comparers.test.ts index 8848b04a6e8..690e3eb4782 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 * as assert from 'assert'; +import assert from 'assert'; import { compareFileExtensions, compareFileExtensionsDefault, compareFileExtensionsLower, compareFileExtensionsUnicode, compareFileExtensionsUpper, compareFileNames, compareFileNamesDefault, compareFileNamesLower, compareFileNamesUnicode, compareFileNamesUpper } from 'vs/base/common/comparers'; diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index f3ad0d55eff..98ad9441a92 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { $, asCssValueWithDefault, h, multibyteAwareBtoa, trackAttributes, copyAttributes, disposableWindowInterval, getWindows, getWindowsCount, getWindowId, getWindowById, hasWindow, getWindow, getDocument } from 'vs/base/browser/dom'; +import assert from 'assert'; +import { $, asCssValueWithDefault, h, multibyteAwareBtoa, trackAttributes, copyAttributes, disposableWindowInterval, getWindows, getWindowsCount, getWindowId, getWindowById, hasWindow, getWindow, getDocument, isHTMLElement } from 'vs/base/browser/dom'; import { ensureCodeWindow, isAuxiliaryWindow, mainWindow } from 'vs/base/browser/window'; import { DeferredPromise, timeout } from 'vs/base/common/async'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; @@ -85,7 +85,7 @@ suite('dom', () => { test('should build simple nodes', () => { const div = $('div'); assert(div); - assert(div instanceof HTMLElement); + assert(isHTMLElement(div)); assert.strictEqual(div.tagName, 'DIV'); assert(!div.firstChild); }); @@ -93,7 +93,7 @@ suite('dom', () => { test('should build nodes with id', () => { const div = $('div#foo'); assert(div); - assert(div instanceof HTMLElement); + assert(isHTMLElement(div)); assert.strictEqual(div.tagName, 'DIV'); assert.strictEqual(div.id, 'foo'); }); @@ -101,7 +101,7 @@ suite('dom', () => { test('should build nodes with class-name', () => { const div = $('div.foo'); assert(div); - assert(div instanceof HTMLElement); + assert(isHTMLElement(div)); assert.strictEqual(div.tagName, 'DIV'); assert.strictEqual(div.className, 'foo'); }); @@ -136,15 +136,15 @@ suite('dom', () => { suite('h', () => { test('should build simple nodes', () => { const div = h('div'); - assert(div.root instanceof HTMLElement); + assert(isHTMLElement(div.root)); assert.strictEqual(div.root.tagName, 'DIV'); const span = h('span'); - assert(span.root instanceof HTMLElement); + assert(isHTMLElement(span.root)); assert.strictEqual(span.root.tagName, 'SPAN'); const img = h('img'); - assert(img.root instanceof HTMLElement); + assert(isHTMLElement(img.root)); assert.strictEqual(img.root.tagName, 'IMG'); }); diff --git a/src/vs/base/test/browser/formattedTextRenderer.test.ts b/src/vs/base/test/browser/formattedTextRenderer.test.ts index 12acef6e7b8..d4ba452786d 100644 --- a/src/vs/base/test/browser/formattedTextRenderer.test.ts +++ b/src/vs/base/test/browser/formattedTextRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { renderFormattedText, renderText } from 'vs/base/browser/formattedTextRenderer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/hash.test.ts b/src/vs/base/test/browser/hash.test.ts index e613a1913f1..d7f319bbe15 100644 --- a/src/vs/base/test/browser/hash.test.ts +++ b/src/vs/base/test/browser/hash.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { sha1Hex } from 'vs/base/browser/hash'; import { hash, StringSHA1 } from 'vs/base/common/hash'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/highlightedLabel.test.ts b/src/vs/base/test/browser/highlightedLabel.test.ts index fe2ceb43d61..c18b2037062 100644 --- a/src/vs/base/test/browser/highlightedLabel.test.ts +++ b/src/vs/base/test/browser/highlightedLabel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/iconLabels.test.ts b/src/vs/base/test/browser/iconLabels.test.ts index c076d4ec38e..2a10f47241b 100644 --- a/src/vs/base/test/browser/iconLabels.test.ts +++ b/src/vs/base/test/browser/iconLabels.test.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; +import { isHTMLElement } from 'vs/base/browser/dom'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -47,7 +48,7 @@ suite('renderLabelWithIcons', () => { const elementsToString = (elements: Array): string => { return elements - .map(elem => elem instanceof HTMLElement ? elem.outerHTML : elem) + .map(elem => isHTMLElement(elem) ? elem.outerHTML : elem) .reduce((a, b) => a + b, ''); }; diff --git a/src/vs/base/test/browser/indexedDB.test.ts b/src/vs/base/test/browser/indexedDB.test.ts index a6266231292..8b1271562fd 100644 --- a/src/vs/base/test/browser/indexedDB.test.ts +++ b/src/vs/base/test/browser/indexedDB.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IndexedDB } from 'vs/base/browser/indexedDB'; import { flakySuite } from 'vs/base/test/common/testUtils'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index f9099c653e9..4ea38c815c3 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { fillInIncompleteTokens, renderMarkdown, renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { marked } from 'vs/base/common/marked/marked'; @@ -607,6 +607,15 @@ const y = 2; assert.deepStrictEqual(newTokens, completeTokens); }); + test(`incomplete ${name} in asterisk list`, () => { + const text = `* list item one\n* list item two and ${delimiter}text`; + const tokens = marked.lexer(text); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(text + delimiter); + assert.deepStrictEqual(newTokens, completeTokens); + }); + test(`incomplete ${name} in numbered list`, () => { const text = `1. list item one\n2. list item two and ${delimiter}text`; const tokens = marked.lexer(text); diff --git a/src/vs/base/test/browser/progressBar.test.ts b/src/vs/base/test/browser/progressBar.test.ts index 9620ae31948..8490fd17897 100644 --- a/src/vs/base/test/browser/progressBar.test.ts +++ b/src/vs/base/test/browser/progressBar.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { mainWindow } from 'vs/base/browser/window'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -17,7 +17,7 @@ suite('ProgressBar', () => { }); teardown(() => { - mainWindow.document.body.removeChild(fixture); + fixture.remove(); }); test('Progress Bar', function () { diff --git a/src/vs/base/test/browser/ui/contextview/contextview.test.ts b/src/vs/base/test/browser/ui/contextview/contextview.test.ts index c302c358937..45fded13807 100644 --- a/src/vs/base/test/browser/ui/contextview/contextview.test.ts +++ b/src/vs/base/test/browser/ui/contextview/contextview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { layout, LayoutAnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/ui/grid/grid.test.ts b/src/vs/base/test/browser/ui/grid/grid.test.ts index 781a0a407f0..92296d1880b 100644 --- a/src/vs/base/test/browser/ui/grid/grid.test.ts +++ b/src/vs/base/test/browser/ui/grid/grid.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { createSerializedGrid, Direction, getRelativeLocation, Grid, GridNode, GridNodeDescriptor, ISerializableView, isGridBranchNode, IViewDeserializer, Orientation, sanitizeGridNodeDescriptor, SerializableGrid, Sizing } from 'vs/base/browser/ui/grid/grid'; import { Event } from 'vs/base/common/event'; import { deepClone } from 'vs/base/common/objects'; diff --git a/src/vs/base/test/browser/ui/grid/gridview.test.ts b/src/vs/base/test/browser/ui/grid/gridview.test.ts index bbc7de0716f..df0c3838e6b 100644 --- a/src/vs/base/test/browser/ui/grid/gridview.test.ts +++ b/src/vs/base/test/browser/ui/grid/gridview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { $ } from 'vs/base/browser/dom'; import { GridView, IView, Orientation, Sizing } from 'vs/base/browser/ui/grid/gridview'; import { nodesToArrays, TestView } from 'vs/base/test/browser/ui/grid/util'; diff --git a/src/vs/base/test/browser/ui/grid/util.ts b/src/vs/base/test/browser/ui/grid/util.ts index 9ebe81b3c56..ccadd7866e6 100644 --- a/src/vs/base/test/browser/ui/grid/util.ts +++ b/src/vs/base/test/browser/ui/grid/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IView } from 'vs/base/browser/ui/grid/grid'; import { GridNode, isGridBranchNode } from 'vs/base/browser/ui/grid/gridview'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/base/test/browser/ui/list/listView.test.ts b/src/vs/base/test/browser/ui/list/listView.test.ts index 7dcab8e7d25..0a6dede20cc 100644 --- a/src/vs/base/test/browser/ui/list/listView.test.ts +++ b/src/vs/base/test/browser/ui/list/listView.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ListView } from 'vs/base/browser/ui/list/listView'; import { range } from 'vs/base/common/arrays'; diff --git a/src/vs/base/test/browser/ui/list/listWidget.test.ts b/src/vs/base/test/browser/ui/list/listWidget.test.ts index 28fd9524341..6d1263dd89c 100644 --- a/src/vs/base/test/browser/ui/list/listWidget.test.ts +++ b/src/vs/base/test/browser/ui/list/listWidget.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { range } from 'vs/base/common/arrays'; diff --git a/src/vs/base/test/browser/ui/list/rangeMap.test.ts b/src/vs/base/test/browser/ui/list/rangeMap.test.ts index 5b3b4a6c65f..0a4625c76b8 100644 --- a/src/vs/base/test/browser/ui/list/rangeMap.test.ts +++ b/src/vs/base/test/browser/ui/list/rangeMap.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { consolidate, groupIntersect, RangeMap } from 'vs/base/browser/ui/list/rangeMap'; import { Range } from 'vs/base/common/range'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/browser/ui/menu/menubar.test.ts b/src/vs/base/test/browser/ui/menu/menubar.test.ts index 49420cc0404..af5e1da2e91 100644 --- a/src/vs/base/test/browser/ui/menu/menubar.test.ts +++ b/src/vs/base/test/browser/ui/menu/menubar.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { $ } from 'vs/base/browser/dom'; import { unthemedMenuStyles } from 'vs/base/browser/ui/menu/menu'; import { MenuBar } from 'vs/base/browser/ui/menu/menubar'; diff --git a/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts b/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts index 2ea6a9c1df9..cc662ab282f 100644 --- a/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts +++ b/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts @@ -3,13 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MouseWheelClassifier } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; export type IMouseWheelEvent = [number, number, number]; suite('MouseWheelClassifier', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('OSX - Apple Magic Mouse', () => { const testData: IMouseWheelEvent[] = [ [1503409622410, -0.025, 0], diff --git a/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts b/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts index 24d4915cca2..39876bc5d91 100644 --- a/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts +++ b/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts @@ -3,10 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('ScrollbarState', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + test('inflates slider size', () => { const actual = new ScrollbarState(0, 14, 0, 339, 42423, 32787); diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index 48ee43fe1b3..7e20426f5a8 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Sash, SashState } from 'vs/base/browser/ui/sash/sash'; import { IView, LayoutPriority, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts index c92a6273336..78b94a9577c 100644 --- a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { AsyncDataTree, CompressibleAsyncDataTree, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; diff --git a/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts index 79de7324a24..9c5777b5fc5 100644 --- a/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { compress, CompressedObjectTreeModel, decompress, ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; import { IObjectTreeModelSetChildrenOptions } from 'vs/base/browser/ui/tree/objectTreeModel'; diff --git a/src/vs/base/test/browser/ui/tree/dataTree.test.ts b/src/vs/base/test/browser/ui/tree/dataTree.test.ts index fb821d3b662..37c586f00e0 100644 --- a/src/vs/base/test/browser/ui/tree/dataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/dataTree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; diff --git a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts index 5657e458849..70277e01c87 100644 --- a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IIndexTreeModelSpliceOptions, IIndexTreeNode, IList, IndexTreeModel } from 'vs/base/browser/ui/tree/indexTreeModel'; import { ITreeElement, ITreeFilter, ITreeNode, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { timeout } from 'vs/base/common/async'; diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index 05594bf561a..eaed09db694 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { CompressibleObjectTree, ICompressibleTreeRenderer, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; diff --git a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts index 4c895d94130..ffef4afb72b 100644 --- a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { ITreeFilter, ITreeNode, ObjectTreeElementCollapseState, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; diff --git a/src/vs/base/test/common/arrays.test.ts b/src/vs/base/test/common/arrays.test.ts index 25a2620a86a..617a766c965 100644 --- a/src/vs/base/test/common/arrays.test.ts +++ b/src/vs/base/test/common/arrays.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as arrays from 'vs/base/common/arrays'; import * as arraysFind from 'vs/base/common/arraysFind'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/arraysFind.test.ts b/src/vs/base/test/common/arraysFind.test.ts index d932b68cbba..caeea0fed22 100644 --- a/src/vs/base/test/common/arraysFind.test.ts +++ b/src/vs/base/test/common/arraysFind.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MonotonousArray, findFirstMonotonous, findLastMonotonous } from 'vs/base/common/arraysFind'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/assert.test.ts b/src/vs/base/test/common/assert.test.ts index ed4e60e8222..f0052bade97 100644 --- a/src/vs/base/test/common/assert.test.ts +++ b/src/vs/base/test/common/assert.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ok } from 'vs/base/common/assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index da309f944bf..e55200ff192 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as async from 'vs/base/common/async'; import * as MicrotaskDelay from "vs/base/common/symbols"; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; diff --git a/src/vs/base/test/common/buffer.test.ts b/src/vs/base/test/common/buffer.test.ts index 6c869a16b3f..e94fd8755a0 100644 --- a/src/vs/base/test/common/buffer.test.ts +++ b/src/vs/base/test/common/buffer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { bufferedStreamToBuffer, bufferToReadable, bufferToStream, decodeBase64, encodeBase64, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer } from 'vs/base/common/buffer'; import { peekStream } from 'vs/base/common/stream'; diff --git a/src/vs/base/test/common/cache.test.ts b/src/vs/base/test/common/cache.test.ts index b1946ed354a..ed4ca02f93b 100644 --- a/src/vs/base/test/common/cache.test.ts +++ b/src/vs/base/test/common/cache.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { Cache } from 'vs/base/common/cache'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/cancellation.test.ts b/src/vs/base/test/common/cancellation.test.ts index 2e184c42267..178f77cde1e 100644 --- a/src/vs/base/test/common/cancellation.test.ts +++ b/src/vs/base/test/common/cancellation.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/charCode.test.ts b/src/vs/base/test/common/charCode.test.ts index 49be69d8578..59c4e3fbcc9 100644 --- a/src/vs/base/test/common/charCode.test.ts +++ b/src/vs/base/test/common/charCode.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/collections.test.ts b/src/vs/base/test/common/collections.test.ts index c2616cc377e..b1304b3f30f 100644 --- a/src/vs/base/test/common/collections.test.ts +++ b/src/vs/base/test/common/collections.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as collections from 'vs/base/common/collections'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -32,4 +32,70 @@ suite('Collections', () => { assert.strictEqual(grouped[group2].length, 1); assert.strictEqual(grouped[group2][0].value, value3); }); + + suite('SetWithKey', () => { + let setWithKey: collections.SetWithKey<{ someProp: string }>; + + const initialValues = ['a', 'b', 'c'].map(s => ({ someProp: s })); + setup(() => { + setWithKey = new collections.SetWithKey<{ someProp: string }>(initialValues, value => value.someProp); + }); + + test('size', () => { + assert.strictEqual(setWithKey.size, 3); + }); + + test('add', () => { + setWithKey.add({ someProp: 'd' }); + assert.strictEqual(setWithKey.size, 4); + assert.strictEqual(setWithKey.has({ someProp: 'd' }), true); + }); + + test('delete', () => { + assert.strictEqual(setWithKey.has({ someProp: 'b' }), true); + setWithKey.delete({ someProp: 'b' }); + assert.strictEqual(setWithKey.size, 2); + assert.strictEqual(setWithKey.has({ someProp: 'b' }), false); + }); + + test('has', () => { + assert.strictEqual(setWithKey.has({ someProp: 'a' }), true); + assert.strictEqual(setWithKey.has({ someProp: 'b' }), true); + }); + + test('entries', () => { + const entries = Array.from(setWithKey.entries()); + assert.deepStrictEqual(entries, initialValues.map(value => [value, value])); + }); + + test('keys and values', () => { + const keys = Array.from(setWithKey.keys()); + const values = Array.from(setWithKey.values()); + assert.deepStrictEqual(keys, initialValues); + assert.deepStrictEqual(values, initialValues); + }); + + test('clear', () => { + setWithKey.clear(); + assert.strictEqual(setWithKey.size, 0); + }); + + test('forEach', () => { + const values: any[] = []; + setWithKey.forEach(value => values.push(value)); + assert.deepStrictEqual(values, initialValues); + }); + + test('iterator', () => { + const values: any[] = []; + for (const value of setWithKey) { + values.push(value); + } + assert.deepStrictEqual(values, initialValues); + }); + + test('toStringTag', () => { + assert.strictEqual(setWithKey[Symbol.toStringTag], 'SetWithKey'); + }); + }); }); diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index 857c394d322..256397a713e 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Color, HSLA, HSVA, RGBA } from 'vs/base/common/color'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/console.test.ts b/src/vs/base/test/common/console.test.ts index 86842d1dcd1..457255eb627 100644 --- a/src/vs/base/test/common/console.test.ts +++ b/src/vs/base/test/common/console.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { getFirstFrame } from 'vs/base/common/console'; import { normalize } from 'vs/base/common/path'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/decorators.test.ts b/src/vs/base/test/common/decorators.test.ts index 92bce8c605b..33d78329dd5 100644 --- a/src/vs/base/test/common/decorators.test.ts +++ b/src/vs/base/test/common/decorators.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { memoize, throttle } from 'vs/base/common/decorators'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/diff/diff.test.ts b/src/vs/base/test/common/diff/diff.test.ts index 353b68ebd7e..eeee6328c83 100644 --- a/src/vs/base/test/common/diff/diff.test.ts +++ b/src/vs/base/test/common/diff/diff.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDiffChange, LcsDiff, StringDiffSequence } from 'vs/base/common/diff/diff'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/errors.test.ts b/src/vs/base/test/common/errors.test.ts index 96210c55314..3ebfaffe4de 100644 --- a/src/vs/base/test/common/errors.test.ts +++ b/src/vs/base/test/common/errors.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 161c7085fa6..3c65d9e77e9 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { stub } from 'sinon'; import { tail2 } from 'vs/base/common/arrays'; import { DeferredPromise, timeout } from 'vs/base/common/async'; diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index c13210daa13..45e411a7e1c 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import * as extpath from 'vs/base/common/extpath'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index 5bfe3856226..5da3552ec56 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { anyScore, createMatches, fuzzyScore, fuzzyScoreGraceful, fuzzyScoreGracefulAggressive, FuzzyScorer, IFilter, IMatch, matchesCamelCase, matchesContiguousSubString, matchesPrefix, matchesStrictPrefix, matchesSubString, matchesWords, or } from 'vs/base/common/filters'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 747e2ee1841..4e41662631d 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { compareItemsByFuzzyScore, FuzzyScore, FuzzyScore2, FuzzyScorerCache, IItemAccessor, IItemScore, pieceToQuery, prepareQuery, scoreFuzzy, scoreFuzzy2, scoreItemFuzzy } from 'vs/base/common/fuzzyScorer'; import { Schemas } from 'vs/base/common/network'; import { basename, dirname, posix, sep, win32 } from 'vs/base/common/path'; diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts index 5bfb3dccfb3..2e721bd3002 100644 --- a/src/vs/base/test/common/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as glob from 'vs/base/common/glob'; import { sep } from 'vs/base/common/path'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/base/test/common/history.test.ts b/src/vs/base/test/common/history.test.ts index 39143609b88..07443052e78 100644 --- a/src/vs/base/test/common/history.test.ts +++ b/src/vs/base/test/common/history.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { HistoryNavigator, HistoryNavigator2 } from 'vs/base/common/history'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/iconLabels.test.ts b/src/vs/base/test/common/iconLabels.test.ts index 4f2cd802c64..4ce6b493afa 100644 --- a/src/vs/base/test/common/iconLabels.test.ts +++ b/src/vs/base/test/common/iconLabels.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IMatch } from 'vs/base/common/filters'; import { escapeIcons, getCodiconAriaLabel, IParsedLabelWithIcons, markdownEscapeEscapedIcons, matchesFuzzyIconAware, parseLabelWithIcons, stripIcons } from 'vs/base/common/iconLabels'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/iterator.test.ts b/src/vs/base/test/common/iterator.test.ts index ce69814ad43..0d95a4afc24 100644 --- a/src/vs/base/test/common/iterator.test.ts +++ b/src/vs/base/test/common/iterator.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Iterable } from 'vs/base/common/iterator'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/json.test.ts b/src/vs/base/test/common/json.test.ts index 4ad121151b4..c8a6202bf06 100644 --- a/src/vs/base/test/common/json.test.ts +++ b/src/vs/base/test/common/json.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { createScanner, Node, parse, ParseError, ParseErrorCode, ParseOptions, parseTree, ScanError, SyntaxKind } from 'vs/base/common/json'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/jsonEdit.test.ts b/src/vs/base/test/common/jsonEdit.test.ts index fa5d41b481a..060884ae9ff 100644 --- a/src/vs/base/test/common/jsonEdit.test.ts +++ b/src/vs/base/test/common/jsonEdit.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { removeProperty, setProperty } from 'vs/base/common/jsonEdit'; import { Edit, FormattingOptions } from 'vs/base/common/jsonFormatter'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/jsonFormatter.test.ts b/src/vs/base/test/common/jsonFormatter.test.ts index 636d987c54a..80d8a22fd35 100644 --- a/src/vs/base/test/common/jsonFormatter.test.ts +++ b/src/vs/base/test/common/jsonFormatter.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as Formatter from 'vs/base/common/jsonFormatter'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/stripComments.test.ts b/src/vs/base/test/common/jsonParse.test.ts similarity index 57% rename from src/vs/base/test/common/stripComments.test.ts rename to src/vs/base/test/common/jsonParse.test.ts index 4aac8cdcb9d..48aa377b2f8 100644 --- a/src/vs/base/test/common/stripComments.test.ts +++ b/src/vs/base/test/common/jsonParse.test.ts @@ -2,14 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; -import { stripComments } from 'vs/base/common/stripComments'; +import { parse, stripComments } from 'vs/base/common/jsonc'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -// We use this regular expression quite often to strip comments in JSON files. - -suite('Strip Comments', () => { +suite('JSON Parse', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('Line comment', () => { @@ -23,7 +21,7 @@ suite('Strip Comments', () => { " \"prop\": 10 ", "}", ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Line comment - EOF', () => { const content: string = [ @@ -36,7 +34,7 @@ suite('Strip Comments', () => { "}", "" ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Line comment - \\r\\n', () => { const content: string = [ @@ -49,7 +47,7 @@ suite('Strip Comments', () => { " \"prop\": 10 ", "}", ].join('\r\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Line comment - EOF - \\r\\n', () => { const content: string = [ @@ -62,7 +60,7 @@ suite('Strip Comments', () => { "}", "" ].join('\r\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Block comment - single line', () => { const content: string = [ @@ -75,7 +73,7 @@ suite('Strip Comments', () => { " \"prop\": 10", "}", ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Block comment - multi line', () => { const content: string = [ @@ -92,7 +90,7 @@ suite('Strip Comments', () => { " \"prop\": 10", "}", ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Block comment - shortest match', () => { const content = "/* abc */ */"; @@ -110,7 +108,7 @@ suite('Strip Comments', () => { " \"/* */\": 10", "}" ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('No strings - single quote', () => { const content: string = [ @@ -136,7 +134,7 @@ suite('Strip Comments', () => { ` "a": 10`, "}" ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); }); test('Trailing comma in array', () => { const content: string = [ @@ -145,6 +143,52 @@ suite('Strip Comments', () => { const expected: string = [ `[ "a", "b", "c" ]` ].join('\n'); - assert.strictEqual(stripComments(content), expected); + assert.deepEqual(parse(content), JSON.parse(expected)); + }); + + test('Trailing comma', () => { + const content: string = [ + "{", + " \"propA\": 10, // a comment", + " \"propB\": false, // a trailing comma", + "}", + ].join('\n'); + const expected = [ + "{", + " \"propA\": 10,", + " \"propB\": false", + "}", + ].join('\n'); + assert.deepEqual(parse(content), JSON.parse(expected)); + }); + + test('Trailing comma - EOF', () => { + const content = ` +// This configuration file allows you to pass permanent command line arguments to VS Code. +// Only a subset of arguments is currently supported to reduce the likelihood of breaking +// the installation. +// +// PLEASE DO NOT CHANGE WITHOUT UNDERSTANDING THE IMPACT +// +// NOTE: Changing this file requires a restart of VS Code. +{ + // Use software rendering instead of hardware accelerated rendering. + // This can help in cases where you see rendering issues in VS Code. + // "disable-hardware-acceleration": true, + // Allows to disable crash reporting. + // Should restart the app if the value is changed. + "enable-crash-reporter": true, + // Unique id used for correlating crash reports sent from this instance. + // Do not edit this value. + "crash-reporter-id": "aaaaab31-7453-4506-97d0-93411b2c21c7", + "locale": "en", + // "log-level": "trace" +} +`; + assert.deepEqual(parse(content), { + "enable-crash-reporter": true, + "crash-reporter-id": "aaaaab31-7453-4506-97d0-93411b2c21c7", + "locale": "en" + }); }); }); diff --git a/src/vs/base/test/common/jsonSchema.test.ts b/src/vs/base/test/common/jsonSchema.test.ts new file mode 100644 index 00000000000..f47e1790143 --- /dev/null +++ b/src/vs/base/test/common/jsonSchema.test.ts @@ -0,0 +1,574 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; +import { getCompressedContent, IJSONSchema } from 'vs/base/common/jsonSchema'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; + +suite('JSON Schema', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('getCompressedContent 1', () => { + + const schema: IJSONSchema = { + type: 'object', + properties: { + a: { + type: 'object', + description: 'a', + properties: { + b: { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + } + }, + e: { + type: 'object', + description: 'e', + properties: { + b: { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + } + } + } + }; + + const expected: IJSONSchema = { + type: 'object', + properties: { + a: { + type: 'object', + description: 'a', + properties: { + b: { + $ref: '#/$defs/_0' + } + } + }, + e: { + type: 'object', + description: 'e', + properties: { + b: { + $ref: '#/$defs/_0' + } + } + } + }, + $defs: { + "_0": { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + } + + }; + + assert.deepEqual(getCompressedContent(schema), JSON.stringify(expected)); + }); + + test('getCompressedContent 2', () => { + + const schema: IJSONSchema = { + type: 'object', + properties: { + a: { + type: 'object', + properties: { + b: { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + } + }, + e: { + type: 'object', + properties: { + b: { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + } + } + } + }; + + const expected: IJSONSchema = { + type: 'object', + properties: { + a: { + $ref: '#/$defs/_0' + + }, + e: { + $ref: '#/$defs/_0' + } + }, + $defs: { + "_0": { + type: 'object', + properties: { + b: { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + } + } + } + + }; + + assert.deepEqual(getCompressedContent(schema), JSON.stringify(expected)); + }); + + test('getCompressedContent 3', () => { + + + const schema: IJSONSchema = { + type: 'object', + properties: { + a: { + type: 'object', + oneOf: [ + { + allOf: [ + { + properties: { + name: { + type: 'string' + }, + description: { + type: 'string' + } + } + }, + { + properties: { + street: { + type: 'string' + }, + } + } + ] + }, + { + allOf: [ + { + properties: { + name: { + type: 'string' + }, + description: { + type: 'string' + } + } + }, + { + properties: { + river: { + type: 'string' + }, + } + } + ] + }, + { + allOf: [ + { + properties: { + name: { + type: 'string' + }, + description: { + type: 'string' + } + } + }, + { + properties: { + mountain: { + type: 'string' + }, + } + } + ] + } + ] + }, + b: { + type: 'object', + properties: { + street: { + properties: { + street: { + type: 'string' + } + } + } + } + } + } + }; + + const expected: IJSONSchema = { + "type": "object", + "properties": { + "a": { + "type": "object", + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/$defs/_0" + }, + { + "$ref": "#/$defs/_1" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/_0" + }, + { + "properties": { + "river": { + "type": "string" + } + } + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/_0" + }, + { + "properties": { + "mountain": { + "type": "string" + } + } + } + ] + } + ] + }, + "b": { + "type": "object", + "properties": { + "street": { + "$ref": "#/$defs/_1" + } + } + } + }, + "$defs": { + "_0": { + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "_1": { + "properties": { + "street": { + "type": "string" + } + } + } + } + }; + + const actual = getCompressedContent(schema); + assert.deepEqual(actual, JSON.stringify(expected)); + }); + + test('getCompressedContent 4', () => { + + const schema: IJSONSchema = { + type: 'object', + properties: { + a: { + type: 'object', + properties: { + b: { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + } + }, + e: { + type: 'object', + properties: { + b: { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + } + }, + f: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + }; + + const expected: IJSONSchema = { + type: 'object', + properties: { + a: { + $ref: '#/$defs/_0' + }, + e: { + $ref: '#/$defs/_0' + }, + f: { + $ref: '#/$defs/_1' + } + }, + $defs: { + "_0": { + type: 'object', + properties: { + b: { + type: 'object', + properties: { + c: { + $ref: '#/$defs/_1' + } + } + } + } + }, + "_1": { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + + }; + + assert.deepEqual(getCompressedContent(schema), JSON.stringify(expected)); + }); + + test('getCompressedContent 5', () => { + + const schema: IJSONSchema = { + type: 'object', + properties: { + a: { + type: 'array', + items: { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + }, + e: { + type: 'array', + items: { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + }, + f: { + type: 'object', + properties: { + b: { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + } + }, + g: { + type: 'object', + properties: { + b: { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + } + } + } + }; + + const expected: IJSONSchema = { + type: 'object', + properties: { + a: { + $ref: '#/$defs/_0' + }, + e: { + $ref: '#/$defs/_0' + }, + f: { + $ref: '#/$defs/_1' + }, + g: { + $ref: '#/$defs/_1' + } + }, + $defs: { + "_0": { + type: 'array', + items: { + $ref: '#/$defs/_2' + } + }, + "_1": { + type: 'object', + properties: { + b: { + $ref: '#/$defs/_2' + } + } + }, + "_2": { + type: 'object', + properties: { + c: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + } + } + } + + }; + + assert.deepEqual(getCompressedContent(schema), JSON.stringify(expected)); + }); + + +}); diff --git a/src/vs/base/test/common/keyCodes.test.ts b/src/vs/base/test/common/keyCodes.test.ts index e3c73412eab..ea539be7c62 100644 --- a/src/vs/base/test/common/keyCodes.test.ts +++ b/src/vs/base/test/common/keyCodes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EVENT_KEY_CODE_MAP, IMMUTABLE_CODE_TO_KEY_CODE, IMMUTABLE_KEY_CODE_TO_CODE, KeyChord, KeyCode, KeyCodeUtils, KeyMod, NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE, ScanCode, ScanCodeUtils } from 'vs/base/common/keyCodes'; import { decodeKeybinding, KeyCodeChord, Keybinding } from 'vs/base/common/keybindings'; import { OperatingSystem } from 'vs/base/common/platform'; diff --git a/src/vs/base/test/common/keybindings.test.ts b/src/vs/base/test/common/keybindings.test.ts index ab26cf24864..5959f510cda 100644 --- a/src/vs/base/test/common/keybindings.test.ts +++ b/src/vs/base/test/common/keybindings.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyCode, ScanCode } from 'vs/base/common/keyCodes'; import { KeyCodeChord, ScanCodeChord } from 'vs/base/common/keybindings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/labels.test.ts b/src/vs/base/test/common/labels.test.ts index 6c9d3eb0f11..b74f0238c16 100644 --- a/src/vs/base/test/common/labels.test.ts +++ b/src/vs/base/test/common/labels.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as labels from 'vs/base/common/labels'; import { isMacintosh, isWindows, OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/base/test/common/lazy.test.ts b/src/vs/base/test/common/lazy.test.ts index 361e3305d43..220d0da6d10 100644 --- a/src/vs/base/test/common/lazy.test.ts +++ b/src/vs/base/test/common/lazy.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Lazy } from 'vs/base/common/lazy'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/lifecycle.test.ts b/src/vs/base/test/common/lifecycle.test.ts index 103009b5219..23080bc8a12 100644 --- a/src/vs/base/test/common/lifecycle.test.ts +++ b/src/vs/base/test/common/lifecycle.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore, dispose, IDisposable, markAsSingleton, ReferenceCollection, SafeDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite, throwIfDisposablesAreLeaked } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/linkedList.test.ts b/src/vs/base/test/common/linkedList.test.ts index 181916d9625..8bd3f6519a7 100644 --- a/src/vs/base/test/common/linkedList.test.ts +++ b/src/vs/base/test/common/linkedList.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { LinkedList } from 'vs/base/common/linkedList'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/linkedText.test.ts b/src/vs/base/test/common/linkedText.test.ts index dc3bdbd0609..63f76d082c4 100644 --- a/src/vs/base/test/common/linkedText.test.ts +++ b/src/vs/base/test/common/linkedText.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { parseLinkedText } from 'vs/base/common/linkedText'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index f92234c3266..f837920f1f9 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { BidirectionalMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, ResourceMap, SetMap, Touch } from 'vs/base/common/map'; import { extUriIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/base/test/common/markdownString.test.ts b/src/vs/base/test/common/markdownString.test.ts index cb7df1696b3..4c402692512 100644 --- a/src/vs/base/test/common/markdownString.test.ts +++ b/src/vs/base/test/common/markdownString.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/marshalling.test.ts b/src/vs/base/test/common/marshalling.test.ts index 7d3bb66cec7..94d30b5b0bf 100644 --- a/src/vs/base/test/common/marshalling.test.ts +++ b/src/vs/base/test/common/marshalling.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { parse, stringify } from 'vs/base/common/marshalling'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/mime.test.ts b/src/vs/base/test/common/mime.test.ts index fce56646ff8..96fa3b6209e 100644 --- a/src/vs/base/test/common/mime.test.ts +++ b/src/vs/base/test/common/mime.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { normalizeMimeType } from 'vs/base/common/mime'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/network.test.ts b/src/vs/base/test/common/network.test.ts index a85b2b392a8..d741b0e4577 100644 --- a/src/vs/base/test/common/network.test.ts +++ b/src/vs/base/test/common/network.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { isWeb } from 'vs/base/common/platform'; import { isEqual } from 'vs/base/common/resources'; diff --git a/src/vs/base/test/common/normalization.test.ts b/src/vs/base/test/common/normalization.test.ts index 65b44eb652c..58f4fe77e81 100644 --- a/src/vs/base/test/common/normalization.test.ts +++ b/src/vs/base/test/common/normalization.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { removeAccents } from 'vs/base/common/normalization'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/numbers.test.ts b/src/vs/base/test/common/numbers.test.ts new file mode 100644 index 00000000000..7095b7aae40 --- /dev/null +++ b/src/vs/base/test/common/numbers.test.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { isPointWithinTriangle } from 'vs/base/common/numbers'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; + +suite('isPointWithinTriangle', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should return true if the point is within the triangle', () => { + const result = isPointWithinTriangle(0.25, 0.25, 0, 0, 1, 0, 0, 1); + assert.ok(result); + }); + + test('should return false if the point is outside the triangle', () => { + const result = isPointWithinTriangle(2, 2, 0, 0, 1, 0, 0, 1); + assert.ok(!result); + }); + + test('should return true if the point is on the edge of the triangle', () => { + const result = isPointWithinTriangle(0.5, 0, 0, 0, 1, 0, 0, 1); + assert.ok(result); + }); +}); diff --git a/src/vs/base/test/common/objects.test.ts b/src/vs/base/test/common/objects.test.ts index 2465585544f..ce600eb0181 100644 --- a/src/vs/base/test/common/objects.test.ts +++ b/src/vs/base/test/common/objects.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as objects from 'vs/base/common/objects'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/observable.test.ts b/src/vs/base/test/common/observable.test.ts index e2694d5c94b..62c7579be12 100644 --- a/src/vs/base/test/common/observable.test.ts +++ b/src/vs/base/test/common/observable.test.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { ISettableObservable, autorun, derived, ITransaction, observableFromEvent, observableValue, transaction, keepObserved, waitForState, autorunHandleChanges, observableSignal } from 'vs/base/common/observable'; import { BaseObservable, IObservable, IObserver } from 'vs/base/common/observableInternal/base'; +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('observables', () => { @@ -1236,6 +1238,35 @@ suite('observables', () => { 'rejected {\"state\":\"error\"}' ]); }); + + test('derived as lazy', () => { + const store = new DisposableStore(); + const log = new Log(); + let i = 0; + const d = derivedDisposable(() => { + const id = i++; + log.log('myDerived ' + id); + return { + dispose: () => log.log(`disposed ${id}`) + }; + }); + + d.get(); + assert.deepStrictEqual(log.getAndClearEntries(), ['myDerived 0', 'disposed 0']); + d.get(); + assert.deepStrictEqual(log.getAndClearEntries(), ['myDerived 1', 'disposed 1']); + + d.keepObserved(store); + assert.deepStrictEqual(log.getAndClearEntries(), []); + d.get(); + assert.deepStrictEqual(log.getAndClearEntries(), ['myDerived 2']); + d.get(); + assert.deepStrictEqual(log.getAndClearEntries(), []); + + store.dispose(); + + assert.deepStrictEqual(log.getAndClearEntries(), ['disposed 2']); + }); }); test('observableValue', () => { diff --git a/src/vs/base/test/common/paging.test.ts b/src/vs/base/test/common/paging.test.ts index f0ff02a4ab5..772083f3613 100644 --- a/src/vs/base/test/common/paging.test.ts +++ b/src/vs/base/test/common/paging.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { disposableTimeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { CancellationError, isCancellationError } from 'vs/base/common/errors'; diff --git a/src/vs/base/test/common/path.test.ts b/src/vs/base/test/common/path.test.ts index 62b43c91d0b..f42abd1fed3 100644 --- a/src/vs/base/test/common/path.test.ts +++ b/src/vs/base/test/common/path.test.ts @@ -27,7 +27,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -import * as assert from 'assert'; +import assert from 'assert'; import * as path from 'vs/base/common/path'; import { isWeb, isWindows } from 'vs/base/common/platform'; import * as process from 'vs/base/common/process'; diff --git a/src/vs/base/test/common/prefixTree.test.ts b/src/vs/base/test/common/prefixTree.test.ts index e5545734a8a..10b77d96bbc 100644 --- a/src/vs/base/test/common/prefixTree.test.ts +++ b/src/vs/base/test/common/prefixTree.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; suite('WellDefinedPrefixTree', () => { diff --git a/src/vs/base/test/common/processes.test.ts b/src/vs/base/test/common/processes.test.ts index d575590ab10..6c0d88e62bb 100644 --- a/src/vs/base/test/common/processes.test.ts +++ b/src/vs/base/test/common/processes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as processes from 'vs/base/common/processes'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/resourceTree.test.ts b/src/vs/base/test/common/resourceTree.test.ts index 9024269a1ef..51fba68f764 100644 --- a/src/vs/base/test/common/resourceTree.test.ts +++ b/src/vs/base/test/common/resourceTree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ResourceTree } from 'vs/base/common/resourceTree'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index d1e4e1025ae..d2315644376 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { toSlashes } from 'vs/base/common/extpath'; import { posix, win32 } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/base/test/common/scrollable.test.ts b/src/vs/base/test/common/scrollable.test.ts index 7059d813daa..41a33727b6e 100644 --- a/src/vs/base/test/common/scrollable.test.ts +++ b/src/vs/base/test/common/scrollable.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SmoothScrollingOperation, SmoothScrollingUpdate } from 'vs/base/common/scrollable'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/skipList.test.ts b/src/vs/base/test/common/skipList.test.ts index 8dadb0e34dd..096bd6a268f 100644 --- a/src/vs/base/test/common/skipList.test.ts +++ b/src/vs/base/test/common/skipList.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { binarySearch } from 'vs/base/common/arrays'; import { SkipList } from 'vs/base/common/skipList'; import { StopWatch } from 'vs/base/common/stopwatch'; diff --git a/src/vs/base/test/common/stream.test.ts b/src/vs/base/test/common/stream.test.ts index 5589dfdceaf..a8473f0ff8f 100644 --- a/src/vs/base/test/common/stream.test.ts +++ b/src/vs/base/test/common/stream.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index 4be439f4656..b84a193eba1 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as strings from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/ternarySearchtree.test.ts b/src/vs/base/test/common/ternarySearchtree.test.ts index c1a8cc771a1..1e684933d46 100644 --- a/src/vs/base/test/common/ternarySearchtree.test.ts +++ b/src/vs/base/test/common/ternarySearchtree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { shuffle } from 'vs/base/common/arrays'; import { randomPath } from 'vs/base/common/extpath'; import { StopWatch } from 'vs/base/common/stopwatch'; diff --git a/src/vs/base/test/common/tfIdf.test.ts b/src/vs/base/test/common/tfIdf.test.ts index 3a423f253a3..df5e37da270 100644 --- a/src/vs/base/test/common/tfIdf.test.ts +++ b/src/vs/base/test/common/tfIdf.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { TfIdfCalculator, TfIdfDocument, TfIdfScore } from 'vs/base/common/tfIdf'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/types.test.ts b/src/vs/base/test/common/types.test.ts index 93e2464925a..f9b894fb73a 100644 --- a/src/vs/base/test/common/types.test.ts +++ b/src/vs/base/test/common/types.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as types from 'vs/base/common/types'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index 69e2abe720d..fa65209a568 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { URI, UriComponents, isUriComponents } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/common/uuid.test.ts b/src/vs/base/test/common/uuid.test.ts index a8defebb567..1cfbf5c12e8 100644 --- a/src/vs/base/test/common/uuid.test.ts +++ b/src/vs/base/test/common/uuid.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as uuid from 'vs/base/common/uuid'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/node/crypto.test.ts b/src/vs/base/test/node/crypto.test.ts index ff929aaa21c..5da7d91f743 100644 --- a/src/vs/base/test/node/crypto.test.ts +++ b/src/vs/base/test/node/crypto.test.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { checksum } from 'vs/base/node/crypto'; @@ -19,7 +20,7 @@ flakySuite('Crypto', () => { setup(function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'crypto'); - return Promises.mkdir(testDir, { recursive: true }); + return fs.promises.mkdir(testDir, { recursive: true }); }); teardown(function () { diff --git a/src/vs/base/test/node/css.build.test.ts b/src/vs/base/test/node/css.build.test.ts index 16c79d41225..4f824b375d9 100644 --- a/src/vs/base/test/node/css.build.test.ts +++ b/src/vs/base/test/node/css.build.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CSSPluginUtilities, rewriteUrls } from 'vs/css.build'; diff --git a/src/vs/base/test/node/extpath.test.ts b/src/vs/base/test/node/extpath.test.ts index 04aa1873295..bf895f6750d 100644 --- a/src/vs/base/test/node/extpath.test.ts +++ b/src/vs/base/test/node/extpath.test.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import * as fs from 'fs'; +import assert from 'assert'; import { tmpdir } from 'os'; import { realcase, realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath'; import { Promises } from 'vs/base/node/pfs'; @@ -16,7 +17,7 @@ flakySuite('Extpath', () => { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'extpath'); - return Promises.mkdir(testDir, { recursive: true }); + return fs.promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/base/test/node/id.test.ts b/src/vs/base/test/node/id.test.ts index 1a629134f06..259b2312c1d 100644 --- a/src/vs/base/test/node/id.test.ts +++ b/src/vs/base/test/node/id.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; import { getMac } from 'vs/base/node/macAddress'; import { flakySuite } from 'vs/base/test/node/testUtils'; diff --git a/src/vs/base/test/node/nodeStreams.test.ts b/src/vs/base/test/node/nodeStreams.test.ts index 8cab1cc8420..44e7dd9154d 100644 --- a/src/vs/base/test/node/nodeStreams.test.ts +++ b/src/vs/base/test/node/nodeStreams.test.ts @@ -5,7 +5,7 @@ import { Writable } from 'stream'; -import * as assert from 'assert'; +import assert from 'assert'; import { StreamSplitter } from 'vs/base/node/nodeStreams'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index b3ef62f232a..1d97e68b689 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import { tmpdir } from 'os'; import { timeout } from 'vs/base/common/async'; @@ -25,7 +25,7 @@ flakySuite('PFS', function () { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'pfs'); - return Promises.mkdir(testDir, { recursive: true }); + return fs.promises.mkdir(testDir, { recursive: true }); }); teardown(() => { @@ -39,7 +39,7 @@ flakySuite('PFS', function () { await Promises.writeFile(testFile, 'Hello World', (null!)); - assert.strictEqual((await Promises.readFile(testFile)).toString(), 'Hello World'); + assert.strictEqual((await fs.promises.readFile(testFile)).toString(), 'Hello World'); }); test('writeFile - parallel write on different files works', async () => { @@ -241,7 +241,7 @@ flakySuite('PFS', function () { const symLink = randomPath(testDir); const copyTarget = randomPath(testDir); - await Promises.mkdir(symbolicLinkTarget, { recursive: true }); + await fs.promises.mkdir(symbolicLinkTarget, { recursive: true }); fs.symlinkSync(symbolicLinkTarget, symLink, 'junction'); @@ -258,7 +258,7 @@ flakySuite('PFS', function () { assert.ok(symbolicLink); assert.ok(!symbolicLink.dangling); - const target = await Promises.readlink(copyTarget); + const target = await fs.promises.readlink(copyTarget); assert.strictEqual(target, symbolicLinkTarget); // Copy does not preserve symlinks if configured as such @@ -294,7 +294,7 @@ flakySuite('PFS', function () { const sourceLinkTestFolder = join(sourceFolder, 'link-test'); // copy-test/link-test const sourceLinkMD5JSFolder = join(sourceLinkTestFolder, 'md5'); // copy-test/link-test/md5 const sourceLinkMD5JSFile = join(sourceLinkMD5JSFolder, 'md5.js'); // copy-test/link-test/md5/md5.js - await Promises.mkdir(sourceLinkMD5JSFolder, { recursive: true }); + await fs.promises.mkdir(sourceLinkMD5JSFolder, { recursive: true }); await Promises.writeFile(sourceLinkMD5JSFile, 'Hello from MD5'); const sourceLinkMD5JSFolderLinked = join(sourceLinkTestFolder, 'md5-linked'); // copy-test/link-test/md5-linked @@ -319,7 +319,7 @@ flakySuite('PFS', function () { assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked)); assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isSymbolicLink()); - const linkTarget = await Promises.readlink(targetLinkMD5JSFolderLinked); + const linkTarget = await fs.promises.readlink(targetLinkMD5JSFolderLinked); assert.strictEqual(linkTarget, targetLinkMD5JSFolder); await Promises.rm(targetLinkTestFolder); @@ -353,7 +353,7 @@ flakySuite('PFS', function () { const directory = randomPath(testDir); const symbolicLink = randomPath(testDir); - await Promises.mkdir(directory, { recursive: true }); + await fs.promises.mkdir(directory, { recursive: true }); fs.symlinkSync(directory, symbolicLink, 'junction'); @@ -369,7 +369,7 @@ flakySuite('PFS', function () { const directory = randomPath(testDir); const symbolicLink = randomPath(testDir); - await Promises.mkdir(directory, { recursive: true }); + await fs.promises.mkdir(directory, { recursive: true }); fs.symlinkSync(directory, symbolicLink, 'junction'); @@ -385,7 +385,7 @@ flakySuite('PFS', function () { const parent = randomPath(join(testDir, 'pfs')); const newDir = join(parent, 'öäü'); - await Promises.mkdir(newDir, { recursive: true }); + await fs.promises.mkdir(newDir, { recursive: true }); assert.ok(fs.existsSync(newDir)); @@ -397,7 +397,7 @@ flakySuite('PFS', function () { test('readdir (with file types)', async () => { if (typeof process.versions['electron'] !== 'undefined' /* needs electron */) { const newDir = join(testDir, 'öäü'); - await Promises.mkdir(newDir, { recursive: true }); + await fs.promises.mkdir(newDir, { recursive: true }); await Promises.writeFile(join(testDir, 'somefile.txt'), 'contents'); diff --git a/src/vs/base/test/node/port.test.ts b/src/vs/base/test/node/port.test.ts index 9230dc84684..048cfc69bd1 100644 --- a/src/vs/base/test/node/port.test.ts +++ b/src/vs/base/test/node/port.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as net from 'net'; import * as ports from 'vs/base/node/ports'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/base/test/node/powershell.test.ts b/src/vs/base/test/node/powershell.test.ts index a710bb80c46..de9b669c769 100644 --- a/src/vs/base/test/node/powershell.test.ts +++ b/src/vs/base/test/node/powershell.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as platform from 'vs/base/common/platform'; import { enumeratePowerShellInstallations, getFirstAvailablePowerShellInstallation, IPowerShellExeDetails } from 'vs/base/node/powershell'; diff --git a/src/vs/base/test/node/processes/processes.integrationTest.ts b/src/vs/base/test/node/processes/processes.integrationTest.ts index ff52bf0205c..f8e827a8594 100644 --- a/src/vs/base/test/node/processes/processes.integrationTest.ts +++ b/src/vs/base/test/node/processes/processes.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as cp from 'child_process'; import { FileAccess } from 'vs/base/common/network'; import * as objects from 'vs/base/common/objects'; diff --git a/src/vs/base/test/node/snapshot.test.ts b/src/vs/base/test/node/snapshot.test.ts index 48621bed95f..c98dfc763cd 100644 --- a/src/vs/base/test/node/snapshot.test.ts +++ b/src/vs/base/test/node/snapshot.test.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { tmpdir } from 'os'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { Promises } from 'vs/base/node/pfs'; @@ -23,7 +24,7 @@ suite('snapshot', () => { setup(function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'snapshot'); - return Promises.mkdir(testDir, { recursive: true }); + return fs.promises.mkdir(testDir, { recursive: true }); }); teardown(function () { @@ -46,8 +47,8 @@ suite('snapshot', () => { const children = await Promises.readdir(dir); for (const child of children) { const p = path.join(dir, child); - if ((await Promises.stat(p)).isFile()) { - const content = await Promises.readFile(p, 'utf-8'); + if ((await fs.promises.stat(p)).isFile()) { + const content = await fs.promises.readFile(p, 'utf-8'); str += `${' '.repeat(indent)}${child}:\n`; for (const line of content.split('\n')) { str += `${' '.repeat(indent + 2)}${line}\n`; diff --git a/src/vs/base/test/node/uri.perf.test.ts b/src/vs/base/test/node/uri.perf.test.ts index 389f3c999dd..cd045f15d0b 100644 --- a/src/vs/base/test/node/uri.perf.test.ts +++ b/src/vs/base/test/node/uri.perf.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { readFileSync } from 'fs'; import { FileAccess } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/base/test/node/zip/zip.test.ts b/src/vs/base/test/node/zip/zip.test.ts index 0a898e2c7bc..0db7981f743 100644 --- a/src/vs/base/test/node/zip/zip.test.ts +++ b/src/vs/base/test/node/zip/zip.test.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import * as fs from 'fs'; +import assert from 'assert'; import { tmpdir } from 'os'; import { createCancelablePromise } from 'vs/base/common/async'; import { FileAccess } from 'vs/base/common/network'; @@ -19,7 +20,7 @@ suite('Zip', () => { test('extract should handle directories', async () => { const testDir = getRandomTestPath(tmpdir(), 'vsctests', 'zip'); - await Promises.mkdir(testDir, { recursive: true }); + await fs.promises.mkdir(testDir, { recursive: true }); const fixtures = FileAccess.asFileUri('vs/base/test/node/zip/fixtures').fsPath; const fixture = path.join(fixtures, 'extract.zip'); diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 700231f0357..019f0b949f0 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -36,25 +36,14 @@ - + + + + diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 46193cdbe97..ca405dd725d 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -10,13 +10,12 @@ import { hostname, release } from 'os'; import { VSBuffer } from 'vs/base/common/buffer'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isSigPipeError, onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; -import { isEqualOrParent } from 'vs/base/common/extpath'; import { Event } from 'vs/base/common/event'; -import { stripComments } from 'vs/base/common/json'; +import { parse } from 'vs/base/common/jsonc'; import { getPathLabel } from 'vs/base/common/labels'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas, VSCODE_AUTHORITY } from 'vs/base/common/network'; -import { isAbsolute, join, posix } from 'vs/base/common/path'; +import { join, posix } from 'vs/base/common/path'; import { IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows, OS } from 'vs/base/common/platform'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -26,7 +25,7 @@ import { getDelayedChannel, ProxyChannel, StaticRouter } from 'vs/base/parts/ipc import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron'; import { Client as MessagePortClient } from 'vs/base/parts/ipc/electron-main/ipc.mp'; import { Server as NodeIPCServer } from 'vs/base/parts/ipc/node/ipc.net'; -import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; +import { IProxyAuthService, ProxyAuthService } from 'vs/platform/native/electron-main/auth'; import { localize } from 'vs/nls'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService'; @@ -52,8 +51,9 @@ import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemPro import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IIssueMainService } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IProcessMainService } from 'vs/platform/issue/common/issue'; import { IssueMainService } from 'vs/platform/issue/electron-main/issueMainService'; +import { ProcessMainService } from 'vs/platform/issue/electron-main/processMainService'; import { IKeyboardLayoutMainService, KeyboardLayoutMainService } from 'vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService'; import { ILaunchMainService, LaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; @@ -121,7 +121,6 @@ import { Lazy } from 'vs/base/common/lazy'; import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows'; import { AuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService'; import { normalizeNFC } from 'vs/base/common/normalization'; - /** * The main VS Code application. There will only ever be one instance, * even if the user starts many instances (e.g. from the command line). @@ -366,7 +365,7 @@ export class CodeApplication extends Disposable { process.on('unhandledRejection', (reason: unknown) => onUnexpectedError(reason)); // Dispose on shutdown - this.lifecycleMainService.onWillShutdown(() => this.dispose()); + Event.once(this.lifecycleMainService.onWillShutdown)(() => this.dispose()); // Contextmenu via IPC support registerContextMenuListener(); @@ -496,24 +495,6 @@ export class CodeApplication extends Disposable { return this.resolveShellEnvironment(args, env, false); }); - validatedIpcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => { - const uri = this.validateNlsPath([path]); - if (!uri || typeof data !== 'string') { - throw new Error('Invalid operation (vscode:writeNlsFile)'); - } - - return this.fileService.writeFile(uri, VSBuffer.fromString(data)); - }); - - validatedIpcMain.handle('vscode:readNlsFile', async (event, ...paths: unknown[]) => { - const uri = this.validateNlsPath(paths); - if (!uri) { - throw new Error('Invalid operation (vscode:readNlsFile)'); - } - - return (await this.fileService.readFile(uri)).value.toString(); - }); - validatedIpcMain.on('vscode:toggleDevTools', event => event.sender.toggleDevTools()); validatedIpcMain.on('vscode:openDevTools', event => event.sender.openDevTools()); @@ -529,26 +510,6 @@ export class CodeApplication extends Disposable { //#endregion } - private validateNlsPath(pathSegments: unknown[]): URI | undefined { - let path: string | undefined = undefined; - - for (const pathSegment of pathSegments) { - if (typeof pathSegment === 'string') { - if (typeof path !== 'string') { - path = pathSegment; - } else { - path = join(path, pathSegment); - } - } - } - - if (typeof path !== 'string' || !isAbsolute(path) || !isEqualOrParent(path, this.environmentMainService.cachedLanguagesPath, !isLinux)) { - return undefined; - } - - return URI.file(path); - } - private onUnexpectedError(error: Error): void { if (error) { @@ -598,7 +559,7 @@ export class CodeApplication extends Disposable { // Main process server (electron IPC based) const mainProcessElectronServer = new ElectronIPCServer(); - this.lifecycleMainService.onWillShutdown(e => { + Event.once(this.lifecycleMainService.onWillShutdown)(e => { if (e.reason === ShutdownReason.KILL) { // When we go down abnormally, make sure to free up // any IPC we accept from other windows to reduce @@ -625,7 +586,7 @@ export class CodeApplication extends Disposable { const appInstantiationService = await this.initServices(machineId, sqmId, devDeviceId, sharedProcessReady); // Auth Handler - this._register(appInstantiationService.createInstance(ProxyAuthHandler)); + appInstantiationService.invokeFunction(accessor => accessor.get(IProxyAuthService)); // Transient profiles handler this._register(appInstantiationService.createInstance(UserDataProfilesHandler)); @@ -1051,6 +1012,9 @@ export class CodeApplication extends Disposable { // Issues services.set(IIssueMainService, new SyncDescriptor(IssueMainService, [this.userEnv])); + // Process + services.set(IProcessMainService, new SyncDescriptor(ProcessMainService, [this.userEnv])); + // Encryption services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService)); @@ -1130,6 +1094,9 @@ export class CodeApplication extends Disposable { // Utility Process Worker services.set(IUtilityProcessWorkerMainService, new SyncDescriptor(UtilityProcessWorkerMainService, undefined, true)); + // Proxy Auth + services.set(IProxyAuthService, new SyncDescriptor(ProxyAuthService)); + // Init services that require it await Promises.settled([ backupMainService.initialize(), @@ -1183,6 +1150,10 @@ export class CodeApplication extends Disposable { const issueChannel = ProxyChannel.fromService(accessor.get(IIssueMainService), disposables); mainProcessElectronServer.registerChannel('issue', issueChannel); + // Process + const processChannel = ProxyChannel.fromService(accessor.get(IProcessMainService), disposables); + mainProcessElectronServer.registerChannel('process', processChannel); + // Encryption const encryptionChannel = ProxyChannel.fromService(accessor.get(IEncryptionMainService), disposables); mainProcessElectronServer.registerChannel('encryption', encryptionChannel); @@ -1394,10 +1365,10 @@ export class CodeApplication extends Disposable { // Crash reporter this.updateCrashReporterEnablement(); + // macOS: rosetta translation warning if (isMacintosh && app.runningUnderARM64Translation) { this.windowsMainService?.sendToFocused('vscode:showTranslatedBuildWarning'); } - } private async installMutex(): Promise { @@ -1437,7 +1408,7 @@ export class CodeApplication extends Disposable { try { const argvContent = await this.fileService.readFile(this.environmentMainService.argvResource); const argvString = argvContent.value.toString(); - const argvJSON = JSON.parse(stripComments(argvString)); + const argvJSON = parse(argvString); const telemetryLevel = getTelemetryLevel(this.configurationService); const enableCrashReporter = telemetryLevel >= TelemetryLevel.CRASH; @@ -1468,6 +1439,9 @@ export class CodeApplication extends Disposable { } } catch (error) { this.logService.error(error); + + // Inform the user via notification + this.windowsMainService?.sendToFocused('vscode:showArgvParseWarning'); } } } diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 8548fa7ce4d..deb0d1eab56 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -6,7 +6,7 @@ import 'vs/platform/update/common/update.config.contribution'; import { app, dialog } from 'electron'; -import { unlinkSync } from 'fs'; +import { unlinkSync, promises } from 'fs'; import { URI } from 'vs/base/common/uri'; import { coalesce, distinct } from 'vs/base/common/arrays'; import { Promises } from 'vs/base/common/async'; @@ -139,7 +139,7 @@ class CodeMain { Event.once(lifecycleMainService.onWillShutdown)(evt => { fileService.dispose(); configurationService.dispose(); - evt.join('instanceLockfile', FSPromises.unlink(environmentMainService.mainLockfile).catch(() => { /* ignored */ })); + evt.join('instanceLockfile', promises.unlink(environmentMainService.mainLockfile).catch(() => { /* ignored */ })); }); return instantiationService.createInstance(CodeApplication, mainProcessNodeIpcServer, instanceEnvironment).startup(); @@ -257,7 +257,7 @@ class CodeMain { environmentMainService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath, environmentMainService.localHistoryHome.with({ scheme: Schemas.file }).fsPath, environmentMainService.backupHome - ].map(path => path ? FSPromises.mkdir(path, { recursive: true }) : undefined)), + ].map(path => path ? promises.mkdir(path, { recursive: true }) : undefined)), // State service stateService.init(), diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js index 8234b734d06..a81bd8c04ed 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js @@ -7,6 +7,10 @@ (function () { 'use strict'; + /** + * @import { ISandboxConfiguration } from '../../../base/parts/sandbox/common/sandboxTypes' + */ + const bootstrapWindow = bootstrapWindowLib(); // Load process explorer into window @@ -21,12 +25,10 @@ }); /** - * @typedef {import('../../../base/parts/sandbox/common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: ISandboxConfiguration) => unknown, + * resultCallback: (result: any, configuration: ISandboxConfiguration) => unknown, * options?: { * configureDeveloperSettings?: (config: ISandboxConfiguration) => { * forceEnableDeveloperKeybindings?: boolean, diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js index 35e8368d3c9..9f41e508bb3 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -9,18 +9,23 @@ (function () { 'use strict'; + /** + * @import {INativeWindowConfiguration} from '../../../platform/window/common/window' + * @import {NativeParsedArgs} from '../../../platform/environment/common/argv' + * @import {ISandboxConfiguration} from '../../../base/parts/sandbox/common/sandboxTypes' + */ + const bootstrapWindow = bootstrapWindowLib(); // Add a perf entry right from the top performance.mark('code/didStartRenderer'); - // Load workbench main JS, CSS and NLS all in parallel. This is an + // Load workbench main JS and CSS all in parallel. This is an // optimization to prevent a waterfall of loading to happen, because // we know for a fact that workbench.desktop.main will depend on - // the related CSS and NLS counterparts. + // the related CSS counterpart. bootstrapWindow.load([ 'vs/workbench/workbench.desktop.main', - 'vs/nls!vs/workbench/workbench.desktop.main', 'vs/css!vs/workbench/workbench.desktop.main' ], function (desktopMain, configuration) { @@ -45,6 +50,7 @@ showSplash(windowConfig); }, beforeLoaderConfig: function (loaderConfig) { + // @ts-ignore loaderConfig.recordStats = true; }, beforeRequire: function (windowConfig) { @@ -74,14 +80,10 @@ //#region Helpers /** - * @typedef {import('../../../platform/window/common/window').INativeWindowConfiguration} INativeWindowConfiguration - * @typedef {import('../../../platform/environment/common/argv').NativeParsedArgs} NativeParsedArgs - * @typedef {import('../../../base/parts/sandbox/common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: INativeWindowConfiguration & NativeParsedArgs) => unknown, + * resultCallback: (result: any, configuration: INativeWindowConfiguration & NativeParsedArgs) => unknown, * options?: { * configureDeveloperSettings?: (config: INativeWindowConfiguration & NativeParsedArgs) => { * forceDisableShowDevtoolsOnError?: boolean, @@ -129,7 +131,9 @@ } // minimal color configuration (works with or without persisted data) - let baseTheme, shellBackground, shellForeground; + let baseTheme; + let shellBackground; + let shellForeground; if (data) { baseTheme = data.baseTheme; shellBackground = data.colorInfo.editorBackground; @@ -162,7 +166,9 @@ style.textContent = `body { background-color: ${shellBackground}; color: ${shellForeground}; margin: 0; padding: 0; }`; // set zoom level as soon as possible + // @ts-ignore if (typeof data?.zoomLevel === 'number' && typeof globalThis.vscode?.webFrame?.setZoomLevel === 'function') { + // @ts-ignore globalThis.vscode.webFrame.setZoomLevel(data.zoomLevel); } @@ -172,9 +178,9 @@ const splash = document.createElement('div'); splash.id = 'monaco-parts-splash'; - splash.className = baseTheme; + splash.className = baseTheme ?? 'vs-dark'; - if (layoutInfo.windowBorder) { + if (layoutInfo.windowBorder && colorInfo.windowBorder) { splash.style.position = 'relative'; splash.style.height = 'calc(100vh - 2px)'; splash.style.width = 'calc(100vw - 2px)'; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 2c1d7afc54c..f940e372ab3 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { hostname, release } from 'os'; import { raceTimeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -13,7 +14,6 @@ import { isAbsolute, join } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { cwd } from 'vs/base/common/process'; import { URI } from 'vs/base/common/uri'; -import { Promises } from 'vs/base/node/pfs'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { IDownloadService } from 'vs/platform/download/common/download'; @@ -124,7 +124,7 @@ class CliMain extends Disposable { await Promise.all([ this.allowWindowsUNCPath(environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath), this.allowWindowsUNCPath(environmentService.extensionsPath) - ].map(path => path ? Promises.mkdir(path, { recursive: true }) : undefined)); + ].map(path => path ? fs.promises.mkdir(path, { recursive: true }) : undefined)); // Logger const loggerService = new LoggerService(getLogLevel(environmentService), environmentService.logsHome); diff --git a/src/vs/code/node/sharedProcess/contrib/codeCacheCleaner.ts b/src/vs/code/node/sharedProcess/contrib/codeCacheCleaner.ts index 77ae5d9786a..7ca147e384d 100644 --- a/src/vs/code/node/sharedProcess/contrib/codeCacheCleaner.ts +++ b/src/vs/code/node/sharedProcess/contrib/codeCacheCleaner.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { RunOnceScheduler } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -54,7 +55,7 @@ export class CodeCacheCleaner extends Disposable { // Delete cache folder if old enough const codeCacheEntryPath = join(codeCacheRootPath, codeCache); - const codeCacheEntryStat = await Promises.stat(codeCacheEntryPath); + const codeCacheEntryStat = await fs.promises.stat(codeCacheEntryPath); if (codeCacheEntryStat.isDirectory() && (now - codeCacheEntryStat.mtime.getTime()) > this._DataMaxAge) { this.logService.trace(`[code cache cleanup]: Removing code cache folder ${codeCache}.`); diff --git a/src/vs/code/node/sharedProcess/contrib/languagePackCachedDataCleaner.ts b/src/vs/code/node/sharedProcess/contrib/languagePackCachedDataCleaner.ts index 68d2cc16661..0c7e517d4bc 100644 --- a/src/vs/code/node/sharedProcess/contrib/languagePackCachedDataCleaner.ts +++ b/src/vs/code/node/sharedProcess/contrib/languagePackCachedDataCleaner.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IStringDictionary } from 'vs/base/common/collections'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -58,7 +59,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { try { const installed: IStringDictionary = Object.create(null); - const metaData: ILanguagePackFile = JSON.parse(await Promises.readFile(join(this.environmentService.userDataPath, 'languagepacks.json'), 'utf8')); + const metaData: ILanguagePackFile = JSON.parse(await fs.promises.readFile(join(this.environmentService.userDataPath, 'languagepacks.json'), 'utf8')); for (const locale of Object.keys(metaData)) { const entry = metaData[locale]; installed[`${entry.hash}.${locale}`] = true; @@ -93,7 +94,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { } const candidate = join(folder, entry); - const stat = await Promises.stat(candidate); + const stat = await fs.promises.stat(candidate); if (stat.isDirectory() && (now - stat.mtime.getTime()) > this._DataMaxAge) { this.logService.trace(`[language pack cache cleanup]: Removing language pack cache folder: ${join(packEntry, entry)}`); diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index f8e915491fc..69c81ac2498 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -71,7 +71,7 @@ import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyn import { UserDataSyncServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncServiceIpc'; import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; -import { NativeUserDataProfileStorageService } from 'vs/platform/userDataProfile/node/userDataProfileStorageService'; +import { SharedProcessUserDataProfileStorageService } from 'vs/platform/userDataProfile/node/userDataProfileStorageService'; import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; @@ -355,7 +355,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { services.set(IUserDataSyncLocalStoreService, new SyncDescriptor(UserDataSyncLocalStoreService, undefined, false /* Eagerly cleans up old backups */)); services.set(IUserDataSyncEnablementService, new SyncDescriptor(UserDataSyncEnablementService, undefined, true)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService, undefined, false /* Initializes the Sync State */)); - services.set(IUserDataProfileStorageService, new SyncDescriptor(NativeUserDataProfileStorageService, undefined, true)); + services.set(IUserDataProfileStorageService, new SyncDescriptor(SharedProcessUserDataProfileStorageService, undefined, true)); services.set(IUserDataSyncResourceProviderService, new SyncDescriptor(UserDataSyncResourceProviderService, undefined, true)); // Signing diff --git a/src/vs/editor/browser/config/charWidthReader.ts b/src/vs/editor/browser/config/charWidthReader.ts index 90bafb66284..e1d36f142bf 100644 --- a/src/vs/editor/browser/config/charWidthReader.ts +++ b/src/vs/editor/browser/config/charWidthReader.ts @@ -56,7 +56,7 @@ class DomCharWidthReader { this._readFromDomElements(); // Remove the container from the DOM - targetWindow.document.body.removeChild(this._container!); + this._container?.remove(); this._container = null; this._testElements = null; diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index c8e5b7e50a4..a5f824ca450 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -950,7 +950,7 @@ function measureText(targetDocument: Document, text: string, fontInfo: FontInfo, const res = regularDomNode.offsetWidth; - targetDocument.body.removeChild(container); + container.remove(); return res; } diff --git a/src/vs/editor/browser/coreCommands.ts b/src/vs/editor/browser/coreCommands.ts index ca0a4bb8a1f..e7f0743af83 100644 --- a/src/vs/editor/browser/coreCommands.ts +++ b/src/vs/editor/browser/coreCommands.ts @@ -30,6 +30,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IViewModel } from 'vs/editor/common/viewModel'; import { ISelection } from 'vs/editor/common/core/selection'; import { getActiveElement } from 'vs/base/browser/dom'; +import { EnterOperation } from 'vs/editor/common/cursor/cursorTypeEditOperations'; const CORE_WEIGHT = KeybindingWeight.EditorCore; @@ -74,7 +75,7 @@ export namespace EditorScroll_ { return true; }; - export const metadata = { + export const metadata: ICommandMetadata = { description: 'Scroll editor in the given direction', args: [ { @@ -252,7 +253,7 @@ export namespace RevealLine_ { return true; }; - export const metadata = { + export const metadata: ICommandMetadata = { description: 'Reveal the given line at the given logical position', args: [ { @@ -1988,7 +1989,7 @@ export namespace CoreEditingCommands { public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: unknown): void { editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineBreakInsert(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); + editor.executeCommands(this.id, EnterOperation.lineBreakInsert(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); } }); diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts new file mode 100644 index 00000000000..195aee2cf4f --- /dev/null +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -0,0 +1,282 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equalsIfDefined, itemsEquals } from 'vs/base/common/equals'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ITransaction, autorun, autorunOpts, autorunWithStoreHandleChanges, derived, derivedOpts, observableFromEvent, observableSignal, observableValue, observableValueOpts } from 'vs/base/common/observable'; +import { TransactionImpl } from 'vs/base/common/observableInternal/base'; +import { derivedWithSetter } from 'vs/base/common/observableInternal/derived'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { EditorOption, FindComputedEditorOptionValueById } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ICursorSelectionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; + +/** + * Returns a facade for the code editor that provides observables for various states/events. +*/ +export function observableCodeEditor(editor: ICodeEditor): ObservableCodeEditor { + return ObservableCodeEditor.get(editor); +} + +export class ObservableCodeEditor extends Disposable { + private static readonly _map = new Map(); + + /** + * Make sure that editor is not disposed yet! + */ + public static get(editor: ICodeEditor): ObservableCodeEditor { + let result = ObservableCodeEditor._map.get(editor); + if (!result) { + result = new ObservableCodeEditor(editor); + ObservableCodeEditor._map.set(editor, result); + const d = editor.onDidDispose(() => { + const item = ObservableCodeEditor._map.get(editor); + if (item) { + ObservableCodeEditor._map.delete(editor); + item.dispose(); + d.dispose(); + } + }); + } + return result; + } + + private _updateCounter = 0; + private _currentTransaction: TransactionImpl | undefined = undefined; + + private _beginUpdate(): void { + this._updateCounter++; + if (this._updateCounter === 1) { + this._currentTransaction = new TransactionImpl(() => { + /** @description Update editor state */ + }); + } + } + + private _endUpdate(): void { + this._updateCounter--; + if (this._updateCounter === 0) { + const t = this._currentTransaction!; + this._currentTransaction = undefined; + t.finish(); + } + } + + private constructor(public readonly editor: ICodeEditor) { + super(); + + this._register(this.editor.onBeginUpdate(() => this._beginUpdate())); + this._register(this.editor.onEndUpdate(() => this._endUpdate())); + + this._register(this.editor.onDidChangeModel(() => { + this._beginUpdate(); + try { + this._model.set(this.editor.getModel(), this._currentTransaction); + this._forceUpdate(); + } finally { + this._endUpdate(); + } + })); + + this._register(this.editor.onDidType((e) => { + this._beginUpdate(); + try { + this._forceUpdate(); + this.onDidType.trigger(this._currentTransaction, e); + } finally { + this._endUpdate(); + } + })); + + this._register(this.editor.onDidChangeModelContent(e => { + this._beginUpdate(); + try { + this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, e); + this._forceUpdate(); + } finally { + this._endUpdate(); + } + })); + + this._register(this.editor.onDidChangeCursorSelection(e => { + this._beginUpdate(); + try { + this._selections.set(this.editor.getSelections(), this._currentTransaction, e); + this._forceUpdate(); + } finally { + this._endUpdate(); + } + })); + } + + public forceUpdate(): void; + public forceUpdate(cb: (tx: ITransaction) => T): T; + public forceUpdate(cb?: (tx: ITransaction) => T): T { + this._beginUpdate(); + try { + this._forceUpdate(); + if (!cb) { return undefined as T; } + return cb(this._currentTransaction!); + } finally { + this._endUpdate(); + } + } + + private _forceUpdate(): void { + this._beginUpdate(); + try { + this._model.set(this.editor.getModel(), this._currentTransaction); + this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, undefined); + this._selections.set(this.editor.getSelections(), this._currentTransaction, undefined); + } finally { + this._endUpdate(); + } + } + + private readonly _model = observableValue(this, this.editor.getModel()); + public readonly model: IObservable = this._model; + + public readonly isReadonly = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly)); + + private readonly _versionId = observableValueOpts({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null); + public readonly versionId: IObservable = this._versionId; + + private readonly _selections = observableValueOpts( + { owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true }, + this.editor.getSelections() ?? null + ); + public readonly selections: IObservable = this._selections; + + + public readonly positions = derivedOpts( + { owner: this, equalsFn: equalsIfDefined(itemsEquals(Position.equals)) }, + reader => this.selections.read(reader)?.map(s => s.getStartPosition()) ?? null + ); + + public readonly isFocused = observableFromEvent(this, e => { + const d1 = this.editor.onDidFocusEditorWidget(e); + const d2 = this.editor.onDidBlurEditorWidget(e); + return { + dispose() { + d1.dispose(); + d2.dispose(); + } + }; + }, () => this.editor.hasWidgetFocus()); + + public readonly value = derivedWithSetter(this, + reader => { this.versionId.read(reader); return this.model.read(reader)?.getValue() ?? ''; }, + (value, tx) => { + const model = this.model.get(); + if (model !== null) { + if (value !== model.getValue()) { + model.setValue(value); + } + } + } + ); + public readonly valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; }); + public readonly cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null); + public readonly cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null); + + public readonly onDidType = observableSignal(this); + + public readonly scrollTop = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollTop()); + public readonly scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft()); + + public readonly layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo()); + + public readonly contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth()); + + public getOption(id: T): IObservable> { + return observableFromEvent(this, cb => this.editor.onDidChangeConfiguration(e => { + if (e.hasChanged(id)) { cb(undefined); } + }), () => this.editor.getOption(id)); + } + + public setDecorations(decorations: IObservable): IDisposable { + const d = new DisposableStore(); + const decorationsCollection = this.editor.createDecorationsCollection(); + d.add(autorunOpts({ owner: this, debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => { + const d = decorations.read(reader); + decorationsCollection.set(d); + })); + d.add({ + dispose: () => { + decorationsCollection.clear(); + } + }); + return d; + } + + private _overlayWidgetCounter = 0; + + public createOverlayWidget(widget: IObservableOverlayWidget): IDisposable { + const overlayWidgetId = 'observableOverlayWidget' + (this._overlayWidgetCounter++); + const w: IOverlayWidget = { + getDomNode: () => widget.domNode, + getPosition: () => widget.position.get(), + getId: () => overlayWidgetId, + allowEditorOverflow: widget.allowEditorOverflow, + getMinContentWidthInPx: () => widget.minContentWidthInPx.get(), + }; + this.editor.addOverlayWidget(w); + const d = autorun(reader => { + widget.position.read(reader); + widget.minContentWidthInPx.read(reader); + this.editor.layoutOverlayWidget(w); + }); + return toDisposable(() => { + d.dispose(); + this.editor.removeOverlayWidget(w); + }); + } +} + +interface IObservableOverlayWidget { + get domNode(): HTMLElement; + readonly position: IObservable; + readonly minContentWidthInPx: IObservable; + get allowEditorOverflow(): boolean; +} + +type RemoveUndefined = T extends undefined ? never : T; +export function reactToChange(observable: IObservable, cb: (value: T, deltas: RemoveUndefined[]) => void): IDisposable { + return autorunWithStoreHandleChanges({ + createEmptyChangeSummary: () => ({ deltas: [] as RemoveUndefined[], didChange: false }), + handleChange: (context, changeSummary) => { + if (context.didChange(observable)) { + const e = context.change; + if (e !== undefined) { + changeSummary.deltas.push(e as RemoveUndefined); + } + changeSummary.didChange = true; + } + return true; + }, + }, (reader, changeSummary) => { + const value = observable.read(reader); + if (changeSummary.didChange) { + cb(value, changeSummary.deltas); + } + }); +} + +export function reactToChangeWithStore(observable: IObservable, cb: (value: T, deltas: RemoveUndefined[], store: DisposableStore) => void): IDisposable { + const store = new DisposableStore(); + const disposable = reactToChange(observable, (value, deltas) => { + store.clear(); + cb(value, deltas, store); + }); + return { + dispose() { + disposable.dispose(); + store.dispose(); + } + }; +} diff --git a/src/vs/editor/browser/observableUtilities.ts b/src/vs/editor/browser/observableUtilities.ts deleted file mode 100644 index 7bcfe7ecd6a..00000000000 --- a/src/vs/editor/browser/observableUtilities.ts +++ /dev/null @@ -1,71 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { autorunOpts, derivedOpts, IObservable, observableFromEvent } from 'vs/base/common/observable'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { Position } from 'vs/editor/common/core/position'; -import { IModelDeltaDecoration } from 'vs/editor/common/model'; - -/** - * Returns a facade for the code editor that provides observables for various states/events. -*/ -export function obsCodeEditor(editor: ICodeEditor): ObservableCodeEditor { - return ObservableCodeEditor.get(editor); -} - -class ObservableCodeEditor { - private static _map = new Map(); - - /** - * Make sure that editor is not disposed yet! - */ - public static get(editor: ICodeEditor): ObservableCodeEditor { - let result = ObservableCodeEditor._map.get(editor); - if (!result) { - result = new ObservableCodeEditor(editor); - ObservableCodeEditor._map.set(editor, result); - const d = editor.onDidDispose(() => { - ObservableCodeEditor._map.delete(editor); - d.dispose(); - }); - } - return result; - } - - private constructor(public readonly editor: ICodeEditor) { - } - - public readonly model = observableFromEvent(this.editor.onDidChangeModel, () => this.editor.getModel()); - public readonly value = observableFromEvent(this.editor.onDidChangeModelContent, () => this.editor.getValue()); - public readonly valueIsEmpty = observableFromEvent(this.editor.onDidChangeModelContent, () => this.editor.getModel()?.getValueLength() === 0); - public readonly selections = observableFromEvent(this.editor.onDidChangeCursorSelection, () => this.editor.getSelections()); - public readonly cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null); - public readonly isFocused = observableFromEvent(e => { - const d1 = this.editor.onDidFocusEditorWidget(e); - const d2 = this.editor.onDidBlurEditorWidget(e); - return { - dispose() { - d1.dispose(); - d2.dispose(); - } - }; - }, () => this.editor.hasWidgetFocus()); - - public setDecorations(decorations: IObservable): IDisposable { - const d = new DisposableStore(); - const decorationsCollection = this.editor.createDecorationsCollection(); - d.add(autorunOpts({ owner: this, debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => { - const d = decorations.read(reader); - decorationsCollection.set(d); - })); - d.add({ - dispose: () => { - decorationsCollection.clear(); - } - }); - return d; - } -} diff --git a/src/vs/editor/browser/services/abstractCodeEditorService.ts b/src/vs/editor/browser/services/abstractCodeEditorService.ts index b960f48191e..1fedb4d17c4 100644 --- a/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -341,7 +341,7 @@ class RefCountedStyleSheet { public unref(): void { this._refCount--; if (this._refCount === 0) { - this._styleSheet.parentNode?.removeChild(this._styleSheet); + this._styleSheet.remove(); this._parent._removeEditorStyleSheets(this._editorId); } } diff --git a/src/vs/editor/browser/services/hoverService/hoverService.ts b/src/vs/editor/browser/services/hoverService/hoverService.ts index 8c5e7319331..20faf6cfa8c 100644 --- a/src/vs/editor/browser/services/hoverService/hoverService.ts +++ b/src/vs/editor/browser/services/hoverService/hoverService.ts @@ -12,7 +12,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { HoverWidget } from 'vs/editor/browser/services/hoverService/hoverWidget'; import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { addDisposableListener, EventType, getActiveElement, isAncestorOfActiveElement, isAncestor, getWindow } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, getActiveElement, isAncestorOfActiveElement, isAncestor, getWindow, isHTMLElement } from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; @@ -20,9 +20,9 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { mainWindow } from 'vs/base/browser/window'; import { ContextViewHandler } from 'vs/platform/contextview/browser/contextViewService'; -import type { IHoverOptions, IHoverWidget, IUpdatableHover, IUpdatableHoverContentOrFactory, IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import type { IHoverOptions, IHoverWidget, IManagedHover, IManagedHoverContentOrFactory, IManagedHoverOptions } from 'vs/base/browser/ui/hover/hover'; import type { IHoverDelegate, IHoverDelegateTarget } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { UpdatableHoverWidget } from 'vs/editor/browser/services/hoverService/updatableHoverWidget'; +import { ManagedHoverWidget } from 'vs/editor/browser/services/hoverService/updatableHoverWidget'; import { TimeoutTimer } from 'vs/base/common/async'; export class HoverService extends Disposable implements IHoverService { @@ -90,7 +90,7 @@ export class HoverService extends Disposable implements IHoverService { }, undefined, hoverDisposables); // Set the container explicitly to enable aux window support if (!options.container) { - const targetElement = options.target instanceof HTMLElement ? options.target : options.target.targetElements[0]; + const targetElement = isHTMLElement(options.target) ? options.target : options.target.targetElements[0]; options.container = this._layoutService.getContainer(getWindow(targetElement)); } @@ -189,22 +189,22 @@ export class HoverService extends Disposable implements IHoverService { } } - private readonly _existingHovers = new Map(); + private readonly _managedHovers = new Map(); // TODO: Investigate performance of this function. There seems to be a lot of content created // and thrown away on start up - setupUpdatableHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IUpdatableHoverContentOrFactory, options?: IUpdatableHoverOptions | undefined): IUpdatableHover { + setupManagedHover(hoverDelegate: IHoverDelegate, targetElement: HTMLElement, content: IManagedHoverContentOrFactory, options?: IManagedHoverOptions | undefined): IManagedHover { - htmlElement.setAttribute('custom-hover', 'true'); + targetElement.setAttribute('custom-hover', 'true'); - if (htmlElement.title !== '') { + if (targetElement.title !== '') { console.warn('HTML element already has a title attribute, which will conflict with the custom hover. Please remove the title attribute.'); - console.trace('Stack trace:', htmlElement.title); - htmlElement.title = ''; + console.trace('Stack trace:', targetElement.title); + targetElement.title = ''; } let hoverPreparation: IDisposable | undefined; - let hoverWidget: UpdatableHoverWidget | undefined; + let hoverWidget: ManagedHoverWidget | undefined; const hideHover = (disposeWidget: boolean, disposePreparation: boolean) => { const hadHover = hoverWidget !== undefined; @@ -225,23 +225,23 @@ export class HoverService extends Disposable implements IHoverService { const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget, trapFocus?: boolean) => { return new TimeoutTimer(async () => { if (!hoverWidget || hoverWidget.isDisposed) { - hoverWidget = new UpdatableHoverWidget(hoverDelegate, target || htmlElement, delay > 0); + hoverWidget = new ManagedHoverWidget(hoverDelegate, target || targetElement, delay > 0); await hoverWidget.update(typeof content === 'function' ? content() : content, focus, { ...options, trapFocus }); } }, delay); }; let isMouseDown = false; - const mouseDownEmitter = addDisposableListener(htmlElement, EventType.MOUSE_DOWN, () => { + const mouseDownEmitter = addDisposableListener(targetElement, EventType.MOUSE_DOWN, () => { isMouseDown = true; hideHover(true, true); }, true); - const mouseUpEmitter = addDisposableListener(htmlElement, EventType.MOUSE_UP, () => { + const mouseUpEmitter = addDisposableListener(targetElement, EventType.MOUSE_UP, () => { isMouseDown = false; }, true); - const mouseLeaveEmitter = addDisposableListener(htmlElement, EventType.MOUSE_LEAVE, (e: MouseEvent) => { + const mouseLeaveEmitter = addDisposableListener(targetElement, EventType.MOUSE_LEAVE, (e: MouseEvent) => { isMouseDown = false; - hideHover(false, (e).fromElement === htmlElement); + hideHover(false, (e).fromElement === targetElement); }, true); const onMouseOver = (e: MouseEvent) => { @@ -252,53 +252,53 @@ export class HoverService extends Disposable implements IHoverService { const toDispose: DisposableStore = new DisposableStore(); const target: IHoverDelegateTarget = { - targetElements: [htmlElement], + targetElements: [targetElement], dispose: () => { } }; if (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse') { // track the mouse position const onMouseMove = (e: MouseEvent) => { target.x = e.x + 10; - if ((e.target instanceof HTMLElement) && getHoverTargetElement(e.target, htmlElement) !== htmlElement) { + if ((isHTMLElement(e.target)) && getHoverTargetElement(e.target, targetElement) !== targetElement) { hideHover(true, true); } }; - toDispose.add(addDisposableListener(htmlElement, EventType.MOUSE_MOVE, onMouseMove, true)); + toDispose.add(addDisposableListener(targetElement, EventType.MOUSE_MOVE, onMouseMove, true)); } hoverPreparation = toDispose; - if ((e.target instanceof HTMLElement) && getHoverTargetElement(e.target as HTMLElement, htmlElement) !== htmlElement) { + if ((isHTMLElement(e.target)) && getHoverTargetElement(e.target as HTMLElement, targetElement) !== targetElement) { return; // Do not show hover when the mouse is over another hover target } toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); }; - const mouseOverDomEmitter = addDisposableListener(htmlElement, EventType.MOUSE_OVER, onMouseOver, true); + const mouseOverDomEmitter = addDisposableListener(targetElement, EventType.MOUSE_OVER, onMouseOver, true); const onFocus = () => { if (isMouseDown || hoverPreparation) { return; } const target: IHoverDelegateTarget = { - targetElements: [htmlElement], + targetElements: [targetElement], dispose: () => { } }; const toDispose: DisposableStore = new DisposableStore(); const onBlur = () => hideHover(true, true); - toDispose.add(addDisposableListener(htmlElement, EventType.BLUR, onBlur, true)); + toDispose.add(addDisposableListener(targetElement, EventType.BLUR, onBlur, true)); toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); hoverPreparation = toDispose; }; // Do not show hover when focusing an input or textarea let focusDomEmitter: undefined | IDisposable; - const tagName = htmlElement.tagName.toLowerCase(); + const tagName = targetElement.tagName.toLowerCase(); if (tagName !== 'input' && tagName !== 'textarea') { - focusDomEmitter = addDisposableListener(htmlElement, EventType.FOCUS, onFocus, true); + focusDomEmitter = addDisposableListener(targetElement, EventType.FOCUS, onFocus, true); } - const hover: IUpdatableHover = { + const hover: IManagedHover = { show: focus => { hideHover(false, true); // terminate a ongoing mouse over preparation triggerShowHover(0, focus, undefined, focus); // show hover immediately @@ -311,7 +311,7 @@ export class HoverService extends Disposable implements IHoverService { await hoverWidget?.update(content, undefined, hoverOptions); }, dispose: () => { - this._existingHovers.delete(htmlElement); + this._managedHovers.delete(targetElement); mouseOverDomEmitter.dispose(); mouseLeaveEmitter.dispose(); mouseDownEmitter.dispose(); @@ -320,19 +320,19 @@ export class HoverService extends Disposable implements IHoverService { hideHover(true, true); } }; - this._existingHovers.set(htmlElement, hover); + this._managedHovers.set(targetElement, hover); return hover; } - triggerUpdatableHover(target: HTMLElement): void { - const hover = this._existingHovers.get(target); + showManagedHover(target: HTMLElement): void { + const hover = this._managedHovers.get(target); if (hover) { hover.show(true); } } public override dispose(): void { - this._existingHovers.forEach(hover => hover.dispose()); + this._managedHovers.forEach(hover => hover.dispose()); super.dispose(); } } diff --git a/src/vs/editor/browser/services/hoverService/hoverWidget.ts b/src/vs/editor/browser/services/hoverService/hoverWidget.ts index 163633fa206..ed929bc965c 100644 --- a/src/vs/editor/browser/services/hoverService/hoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/hoverWidget.ts @@ -150,7 +150,7 @@ export class HoverWidget extends Widget implements IHoverWidget { contentsElement.textContent = options.content; contentsElement.style.whiteSpace = 'pre-wrap'; - } else if (options.content instanceof HTMLElement) { + } else if (dom.isHTMLElement(options.content)) { contentsElement.appendChild(options.content); contentsElement.classList.add('html-hover-contents'); diff --git a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts index 762b6626aff..cf9b355831f 100644 --- a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IHoverWidget, IUpdatableHoverContent, IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import { isHTMLElement } from 'vs/base/browser/dom'; +import type { IHoverWidget, IManagedHoverContent, IManagedHoverOptions } from 'vs/base/browser/ui/hover/hover'; import type { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/hover/hoverDelegate'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -12,9 +13,9 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { isFunction, isString } from 'vs/base/common/types'; import { localize } from 'vs/nls'; -type IUpdatableHoverResolvedContent = IMarkdownString | string | HTMLElement | undefined; +type IManagedHoverResolvedContent = IMarkdownString | string | HTMLElement | undefined; -export class UpdatableHoverWidget implements IDisposable { +export class ManagedHoverWidget implements IDisposable { private _hoverWidget: IHoverWidget | undefined; private _cancellationTokenSource: CancellationTokenSource | undefined; @@ -22,7 +23,7 @@ export class UpdatableHoverWidget implements IDisposable { constructor(private hoverDelegate: IHoverDelegate, private target: IHoverDelegateTarget | HTMLElement, private fadeInAnimation: boolean) { } - async update(content: IUpdatableHoverContent, focus?: boolean, options?: IUpdatableHoverOptions): Promise { + async update(content: IManagedHoverContent, focus?: boolean, options?: IManagedHoverOptions): Promise { if (this._cancellationTokenSource) { // there's an computation ongoing, cancel it this._cancellationTokenSource.dispose(true); @@ -33,7 +34,7 @@ export class UpdatableHoverWidget implements IDisposable { } let resolvedContent; - if (content === undefined || isString(content) || content instanceof HTMLElement) { + if (content === undefined || isString(content) || isHTMLElement(content)) { resolvedContent = content; } else if (!isFunction(content.markdown)) { resolvedContent = content.markdown ?? content.markdownNotSupportedFallback; @@ -63,7 +64,7 @@ export class UpdatableHoverWidget implements IDisposable { this.show(resolvedContent, focus, options); } - private show(content: IUpdatableHoverResolvedContent, focus?: boolean, options?: IUpdatableHoverOptions): void { + private show(content: IManagedHoverResolvedContent, focus?: boolean, options?: IManagedHoverOptions): void { const oldHoverWidget = this._hoverWidget; if (this.hasContent(content)) { @@ -85,7 +86,7 @@ export class UpdatableHoverWidget implements IDisposable { oldHoverWidget?.dispose(); } - private hasContent(content: IUpdatableHoverResolvedContent): content is NonNullable { + private hasContent(content: IManagedHoverResolvedContent): content is NonNullable { if (!content) { return false; } diff --git a/src/vs/editor/browser/stableEditorScroll.ts b/src/vs/editor/browser/stableEditorScroll.ts index 986e18ac6c0..4d7539435cd 100644 --- a/src/vs/editor/browser/stableEditorScroll.ts +++ b/src/vs/editor/browser/stableEditorScroll.ts @@ -20,6 +20,16 @@ export class StableEditorScrollState { const visibleRanges = editor.getVisibleRanges(); if (visibleRanges.length > 0) { visiblePosition = visibleRanges[0].getStartPosition(); + + const cursorPos = editor.getPosition(); + if (cursorPos) { + const isVisible = visibleRanges.some(range => range.containsPosition(cursorPos)); + if (isVisible) { + // Keep cursor pos fixed if it is visible + visiblePosition = cursorPos; + } + } + const visiblePositionScrollTop = editor.getTopForPosition(visiblePosition.lineNumber, visiblePosition.column); visiblePositionScrollDelta = editor.getScrollTop() - visiblePositionScrollTop; } diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 64fb2185eed..2861fd8f82a 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -184,7 +184,7 @@ function createLineBreaks(targetWindow: Window, requests: string[], fontInfo: Fo result[i] = new ModelLineProjectionData(injectionOffsets, injectionOptions, breakOffsets, breakOffsetsVisibleColumn, wrappedTextIndentLength); } - targetWindow.document.body.removeChild(containerDomNode); + containerDomNode.remove(); return result; } diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index c7befc25e51..dc04a0d9ea2 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -309,9 +309,7 @@ export class VisibleLinesCollection { // Remove from DOM for (let i = 0, len = deleted.length; i < len; i++) { const lineDomNode = deleted[i].getDomNode(); - if (lineDomNode) { - this.domNode.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } } @@ -324,9 +322,7 @@ export class VisibleLinesCollection { // Remove from DOM for (let i = 0, len = deleted.length; i < len; i++) { const lineDomNode = deleted[i].getDomNode(); - if (lineDomNode) { - this.domNode.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } } @@ -515,9 +511,7 @@ class ViewLayerRenderer { private _removeLinesBefore(ctx: IRendererContext, removeCount: number): void { for (let i = 0; i < removeCount; i++) { const lineDomNode = ctx.lines[i].getDomNode(); - if (lineDomNode) { - this.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } ctx.lines.splice(0, removeCount); } @@ -536,9 +530,7 @@ class ViewLayerRenderer { for (let i = 0; i < removeCount; i++) { const lineDomNode = ctx.lines[removeIndex + i].getDomNode(); - if (lineDomNode) { - this.domNode.removeChild(lineDomNode); - } + lineDomNode?.remove(); } ctx.lines.splice(removeIndex, removeCount); } diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index a11435b8e73..bdf8eb77d21 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -121,7 +121,7 @@ export class ViewContentWidgets extends ViewPart { delete this._widgets[widgetId]; const domNode = myWidget.domNode.domNode; - domNode.parentNode!.removeChild(domNode); + domNode.remove(); domNode.removeAttribute('monaco-visible-content-widget'); this.setShouldRender(); diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 48c79a783ea..164b2299b75 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -243,7 +243,7 @@ export class GlyphMarginWidgets extends ViewPart { const domNode = widgetData.domNode.domNode; delete this._widgets[widgetId]; - domNode.parentNode?.removeChild(domNode); + domNode.remove(); this.setShouldRender(); } } diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 427df7cd3d8..7b49f5b0dea 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -94,6 +94,10 @@ class MinimapOptions { public readonly minimapCharWidth: number; public readonly sectionHeaderFontFamily: string; public readonly sectionHeaderFontSize: number; + /** + * Space in between the characters of the section header (in CSS px) + */ + public readonly sectionHeaderLetterSpacing: number; public readonly sectionHeaderFontColor: RGBA8; public readonly charRenderer: () => MinimapCharRenderer; @@ -139,6 +143,7 @@ class MinimapOptions { this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale; this.sectionHeaderFontFamily = DEFAULT_FONT_FAMILY; this.sectionHeaderFontSize = minimapOpts.sectionHeaderFontSize * pixelRatio; + this.sectionHeaderLetterSpacing = minimapOpts.sectionHeaderLetterSpacing; // intentionally not multiplying by pixelRatio this.sectionHeaderFontColor = MinimapOptions._getSectionHeaderColor(theme, tokensColorTracker.getColor(ColorId.DefaultForeground)); this.charRenderer = createSingleCallFunction(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); @@ -196,6 +201,7 @@ class MinimapOptions { && this.minimapLineHeight === other.minimapLineHeight && this.minimapCharWidth === other.minimapCharWidth && this.sectionHeaderFontSize === other.sectionHeaderFontSize + && this.sectionHeaderLetterSpacing === other.sectionHeaderLetterSpacing && this.defaultBackgroundColor && this.defaultBackgroundColor.equals(other.defaultBackgroundColor) && this.backgroundColor && this.backgroundColor.equals(other.backgroundColor) && this.foregroundAlpha === other.foregroundAlpha @@ -1788,6 +1794,7 @@ class InnerMinimap extends Disposable { private _renderSectionHeaders(layout: MinimapLayout) { const minimapLineHeight = this._model.options.minimapLineHeight; const sectionHeaderFontSize = this._model.options.sectionHeaderFontSize; + const sectionHeaderLetterSpacing = this._model.options.sectionHeaderLetterSpacing; const backgroundFillHeight = sectionHeaderFontSize * 1.5; const { canvasInnerWidth } = this._model.options; @@ -1798,7 +1805,8 @@ class InnerMinimap extends Disposable { const separatorStroke = foregroundFill; const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!; - canvasContext.font = sectionHeaderFontSize + 'px ' + this._model.options.sectionHeaderFontFamily; + canvasContext.letterSpacing = sectionHeaderLetterSpacing + 'px'; + canvasContext.font = '500 ' + sectionHeaderFontSize + 'px ' + this._model.options.sectionHeaderFontFamily; canvasContext.strokeStyle = separatorStroke; canvasContext.lineWidth = 0.2; diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 37914a70335..a99dac77cde 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -271,12 +271,12 @@ export class ViewZones extends ViewPart { zone.domNode.removeAttribute('monaco-visible-view-zone'); zone.domNode.removeAttribute('monaco-view-zone'); - zone.domNode.domNode.parentNode!.removeChild(zone.domNode.domNode); + zone.domNode.domNode.remove(); if (zone.marginDomNode) { zone.marginDomNode.removeAttribute('monaco-visible-view-zone'); zone.marginDomNode.removeAttribute('monaco-view-zone'); - zone.marginDomNode.domNode.parentNode!.removeChild(zone.marginDomNode.domNode); + zone.marginDomNode.domNode.remove(); } this.setShouldRender(); diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 07753688f7e..f8ed64fba6c 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -1613,7 +1613,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE public setBanner(domNode: HTMLElement | null, domNodeHeight: number): void { if (this._bannerDomNode && this._domElement.contains(this._bannerDomNode)) { - this._domElement.removeChild(this._bannerDomNode); + this._bannerDomNode.remove(); } this._bannerDomNode = domNode; @@ -1648,6 +1648,16 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this.languageConfigurationService, this._themeService, attachedView, + { + batchChanges: (cb) => { + try { + this._beginUpdate(); + return cb(); + } finally { + this._endUpdate(); + } + }, + } ); // Someone might destroy the model from under the editor, so prevent any exceptions by setting a null model @@ -1874,10 +1884,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._domElement.removeAttribute('data-mode-id'); if (removeDomNode && this._domElement.contains(removeDomNode)) { - this._domElement.removeChild(removeDomNode); + removeDomNode.remove(); } if (this._bannerDomNode && this._domElement.contains(this._bannerDomNode)) { - this._domElement.removeChild(this._bannerDomNode); + this._bannerDomNode.remove(); } return model; } diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts index db99842d621..55b5d4a1e54 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts @@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IReader, autorunHandleChanges, derived, derivedOpts, observableFromEvent } from 'vs/base/common/observable'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; -import { obsCodeEditor } from 'vs/editor/browser/observableUtilities'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; @@ -27,18 +27,18 @@ export class DiffEditorEditors extends Disposable { private readonly _onDidContentSizeChange = this._register(new Emitter()); public get onDidContentSizeChange() { return this._onDidContentSizeChange.event; } - public readonly modifiedScrollTop = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop()); - public readonly modifiedScrollHeight = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight()); + public readonly modifiedScrollTop = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop()); + public readonly modifiedScrollHeight = observableFromEvent(this, this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight()); - public readonly modifiedModel = obsCodeEditor(this.modified).model; + public readonly modifiedModel = observableCodeEditor(this.modified).model; - public readonly modifiedSelections = observableFromEvent(this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []); + public readonly modifiedSelections = observableFromEvent(this, this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []); public readonly modifiedCursor = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1)); - public readonly originalCursor = observableFromEvent(this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1)); + public readonly originalCursor = observableFromEvent(this, this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1)); - public readonly isOriginalFocused = obsCodeEditor(this.original).isFocused; - public readonly isModifiedFocused = obsCodeEditor(this.modified).isFocused; + public readonly isOriginalFocused = observableCodeEditor(this.original).isFocused; + public readonly isModifiedFocused = observableCodeEditor(this.modified).isFocused; public readonly isFocused = derived(this, reader => this.isOriginalFocused.read(reader) || this.isModifiedFocused.read(reader)); diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts index 23a75bac47d..6a5283f2491 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts @@ -81,7 +81,7 @@ export class DiffEditorViewZones extends Disposable { })); const originalModelTokenizationCompleted = this._diffModel.map(m => - m ? observableFromEvent(m.model.original.onDidChangeTokens, () => m.model.original.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) : undefined + m ? observableFromEvent(this, m.model.original.onDidChangeTokens, () => m.model.original.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) : undefined ).map((m, reader) => m?.read(reader)); const alignments = derived((reader) => { @@ -525,13 +525,15 @@ function computeRangeAlignment( let lastModLineNumber = c.modified.startLineNumber; let lastOrigLineNumber = c.original.startLineNumber; - function emitAlignment(origLineNumberExclusive: number, modLineNumberExclusive: number) { + function emitAlignment(origLineNumberExclusive: number, modLineNumberExclusive: number, forceAlignment = false) { if (origLineNumberExclusive < lastOrigLineNumber || modLineNumberExclusive < lastModLineNumber) { return; } if (first) { first = false; - } else if (origLineNumberExclusive === lastOrigLineNumber || modLineNumberExclusive === lastModLineNumber) { + } else if (!forceAlignment && (origLineNumberExclusive === lastOrigLineNumber || modLineNumberExclusive === lastModLineNumber)) { + // This causes a re-alignment of an already aligned line. + // However, we don't care for the final alignment. return; } const originalRange = new LineRange(lastOrigLineNumber, origLineNumberExclusive); @@ -575,7 +577,7 @@ function computeRangeAlignment( } } - emitAlignment(c.original.endLineNumberExclusive, c.modified.endLineNumberExclusive); + emitAlignment(c.original.endLineNumberExclusive, c.modified.endLineNumberExclusive, true); lastOriginalLineNumber = c.original.endLineNumberExclusive; lastModifiedLineNumber = c.modified.endLineNumberExclusive; diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts index f5bbdb1b43f..b53bb2660e3 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts @@ -16,7 +16,7 @@ export class DiffEditorOptions { private readonly _diffEditorWidth = observableValue(this, 0); - private readonly _screenReaderMode = observableFromEvent(this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); + private readonly _screenReaderMode = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); constructor( options: Readonly, diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts index b1520c8821f..67f8505cc28 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts @@ -8,7 +8,8 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IObservable, IReader, ISettableObservable, ITransaction, autorun, autorunWithStore, derived, observableSignal, observableSignalFromEvent, observableValue, transaction, waitForState } from 'vs/base/common/observable'; import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; -import { filterWithPrevious, readHotReloadableExport } from 'vs/editor/browser/widget/diffEditor/utils'; +import { filterWithPrevious } from 'vs/editor/browser/widget/diffEditor/utils'; +import { readHotReloadableExport } from 'vs/base/common/hotReloadHelpers'; import { ISerializedLineRange, LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange'; import { DefaultLinesDiffComputer } from 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'; import { IDocumentDiff } from 'vs/editor/common/diff/documentDiffProvider'; diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index cd5c2e8b606..9c239778773 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -5,7 +5,7 @@ import { getWindow, h } from 'vs/base/browser/dom'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; import { findLast } from 'vs/base/common/arraysFind'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { toDisposable } from 'vs/base/common/lifecycle'; import { IObservable, ITransaction, autorun, autorunWithStore, derived, observableFromEvent, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from 'vs/base/common/observable'; @@ -26,7 +26,8 @@ import { HideUnchangedRegionsFeature } from 'vs/editor/browser/widget/diffEditor import { MovedBlocksLinesFeature } from 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; import { RevertButtonsFeature } from 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature'; -import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { readHotReloadableExport } from 'vs/base/common/hotReloadHelpers'; import { bindContextKey } from 'vs/platform/observable/common/platformObservableUtils'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; @@ -111,7 +112,7 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { this._contextKeyService.createKey('isInDiffEditor', true); this._domElement.appendChild(this.elements.root); - this._register(toDisposable(() => this._domElement.removeChild(this.elements.root))); + this._register(toDisposable(() => this.elements.root.remove())); this._rootSizeObserver = this._register(new ObservableElementSizeObserver(this.elements.root, options.dimension)); this._rootSizeObserver.setAutomaticLayout(options.automaticLayout ?? false); @@ -326,6 +327,17 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { this._register(autorunWithStore((reader, store) => { store.add(new (readHotReloadableExport(RevertButtonsFeature, reader))(this._editors, this._diffModel, this._options, this)); })); + + this._register(autorunWithStore((reader, store) => { + const model = this._diffModel.read(reader); + if (!model) { return; } + for (const m of [model.model.original, model.model.modified]) { + store.add(m.onWillDispose(e => { + onUnexpectedError(new BugIndicatingError('TextModel got disposed before DiffEditorWidget model got reset')); + this.setModel(null); + })); + } + })); } public getViewWidth(): number { @@ -345,6 +357,12 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { const fullWidth = this._rootSizeObserver.width.read(reader); const fullHeight = this._rootSizeObserver.height.read(reader); + if (this._rootSizeObserver.automaticLayout) { + this.elements.root.style.height = '100%'; + } else { + this.elements.root.style.height = fullHeight + 'px'; + } + const sash = this._sash.read(reader); const gutter = this._gutter.read(reader); diff --git a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts index e6ba7e45ce1..010fba5ec96 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts @@ -37,7 +37,7 @@ const width = 35; export class DiffEditorGutter extends Disposable { private readonly _menu = this._register(this._menuService.createMenu(MenuId.DiffEditorHunkToolbar, this._contextKeyService)); - private readonly _actions = observableFromEvent(this._menu.onDidChange, () => this._menu.getActions()); + private readonly _actions = observableFromEvent(this, this._menu.onDidChange, () => this._menu.getActions()); private readonly _hasActions = this._actions.map(a => a.length > 0); private readonly _showSash = derived(this, reader => this._options.renderSideBySide.read(reader) && this._hasActions.read(reader)); @@ -278,9 +278,6 @@ class DiffToolBar extends Disposable implements IGutterItemView { // Item might have changed itemHeight = this._elements.buttons.clientHeight; - this._elements.root.style.top = itemRange.start + 'px'; - this._elements.root.style.height = itemRange.length + 'px'; - const middleHeight = itemRange.length / 2 - itemHeight / 2; const margin = itemHeight; diff --git a/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts index 7d0d3a992c1..afb29693f81 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts @@ -26,8 +26,8 @@ export class MovedBlocksLinesFeature extends Disposable { public static readonly movedCodeBlockPadding = 4; private readonly _element: SVGElement; - private readonly _originalScrollTop = observableFromEvent(this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop()); - private readonly _modifiedScrollTop = observableFromEvent(this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop()); + private readonly _originalScrollTop = observableFromEvent(this, this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop()); + private readonly _modifiedScrollTop = observableFromEvent(this, this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop()); private readonly _viewZonesChanged = observableSignalFromEvent('onDidChangeViewZones', this._editors.modified.onDidChangeViewZones); public readonly width = observableValue(this, 0); diff --git a/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts index 8141cd9452c..017d8268f6e 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/overviewRulerFeature.ts @@ -23,7 +23,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; export class OverviewRulerFeature extends Disposable { private static readonly ONE_OVERVIEW_WIDTH = 15; - public static readonly ENTIRE_DIFF_OVERVIEW_WIDTH = OverviewRulerFeature.ONE_OVERVIEW_WIDTH * 2; + public static readonly ENTIRE_DIFF_OVERVIEW_WIDTH = this.ONE_OVERVIEW_WIDTH * 2; public readonly width = OverviewRulerFeature.ENTIRE_DIFF_OVERVIEW_WIDTH; constructor( diff --git a/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts b/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts index 36bd4d465ff..80b553bcb30 100644 --- a/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts +++ b/src/vs/editor/browser/widget/diffEditor/registrations.contribution.ts @@ -12,13 +12,13 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; export const diffMoveBorder = registerColor( 'diffEditor.move.border', - { dark: '#8b8b8b9c', light: '#8b8b8b9c', hcDark: '#8b8b8b9c', hcLight: '#8b8b8b9c', }, + '#8b8b8b9c', localize('diffEditor.move.border', 'The border color for text that got moved in the diff editor.') ); export const diffMoveBorderActive = registerColor( 'diffEditor.moveActive.border', - { dark: '#FFA500', light: '#FFA500', hcDark: '#FFA500', hcLight: '#FFA500', }, + '#FFA500', localize('diffEditor.moveActive.border', 'The active border color for text that got moved in the diff editor.') ); diff --git a/src/vs/editor/browser/widget/diffEditor/style.css b/src/vs/editor/browser/widget/diffEditor/style.css index 49ad115e36b..ebb52234658 100644 --- a/src/vs/editor/browser/widget/diffEditor/style.css +++ b/src/vs/editor/browser/widget/diffEditor/style.css @@ -328,6 +328,10 @@ flex-shrink: 0; flex-grow: 0; + & > div { + position: absolute; + } + .gutterItem { opacity: 0; transition: opacity 0.7s; @@ -374,8 +378,7 @@ .actions-container { width: fit-content; border-radius: 4px; - border: 1px var(--vscode-menu-border) solid; - background: var(--vscode-editor-background); + background: var(--vscode-editorGutter-commentRangeForeground); .action-item { &:hover { @@ -383,7 +386,7 @@ } .action-label { - padding: 0.5px 1px; + padding: 1px 2px; } } } diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index 3b968353291..1e6d5eee428 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -6,9 +6,8 @@ import { IDimension } from 'vs/base/browser/dom'; import { findLast } from 'vs/base/common/arraysFind'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { isHotReloadEnabled, registerHotReloadHandler } from 'vs/base/common/hotReload'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { IObservable, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableValue, transaction } from 'vs/base/common/observable'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { ICodeEditor, IOverlayWidget, IViewZone } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; @@ -76,14 +75,14 @@ export function applyObservableDecorations(editor: ICodeEditor, decorations: IOb export function appendRemoveOnDispose(parent: HTMLElement, child: HTMLElement) { parent.appendChild(child); return toDisposable(() => { - parent.removeChild(child); + child.remove(); }); } export function prependRemoveOnDispose(parent: HTMLElement, child: HTMLElement) { parent.prepend(child); return toDisposable(() => { - parent.removeChild(child); + child.remove(); }); } @@ -96,6 +95,9 @@ export class ObservableElementSizeObserver extends Disposable { private readonly _height: ISettableObservable; public get height(): IObservable { return this._height; } + private _automaticLayout: boolean = false; + public get automaticLayout(): boolean { return this._automaticLayout; } + constructor(element: HTMLElement | null, dimension: IDimension | undefined) { super(); @@ -115,6 +117,7 @@ export class ObservableElementSizeObserver extends Disposable { } public setAutomaticLayout(automaticLayout: boolean): void { + this._automaticLayout = automaticLayout; if (automaticLayout) { this.elementSizeObserver.startObserving(); } else { @@ -294,29 +297,6 @@ export function applyStyle(domNode: HTMLElement, style: Partial<{ [TKey in keyof }); } -export function readHotReloadableExport(value: T, reader: IReader | undefined): T { - observeHotReloadableExports([value], reader); - return value; -} - -export function observeHotReloadableExports(values: any[], reader: IReader | undefined): void { - if (isHotReloadEnabled()) { - const o = observableSignalFromEvent( - 'reload', - event => registerHotReloadHandler(({ oldExports }) => { - if (![...Object.values(oldExports)].some(v => values.includes(v))) { - return undefined; - } - return (_newExports) => { - event(undefined); - return true; - }; - }) - ); - o.read(reader); - } -} - export function applyViewZones(editor: ICodeEditor, viewZones: IObservable, setIsUpdating?: (isUpdatingViewZones: boolean) => void, zoneIds?: Set): IDisposable { const store = new DisposableStore(); const lastViewZoneIds: string[] = []; diff --git a/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts index 1c3341a73ef..a301fc6124b 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts @@ -11,12 +11,12 @@ import { LineRange } from 'vs/editor/common/core/lineRange'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; export class EditorGutter extends Disposable { - private readonly scrollTop = observableFromEvent( + private readonly scrollTop = observableFromEvent(this, this._editor.onDidScrollChange, (e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop() ); private readonly isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0); - private readonly modelAttached = observableFromEvent( + private readonly modelAttached = observableFromEvent(this, this._editor.onDidChangeModel, (e) => /** @description editor.onDidChangeModel */ this._editor.hasModel() ); @@ -136,7 +136,7 @@ export class EditorGutter extends D for (const id of unusedIds) { const view = this.views.get(id)!; view.gutterItemView.dispose(); - this._domNode.removeChild(view.domNode); + view.domNode.remove(); this.views.delete(id); } } diff --git a/src/vs/editor/browser/widget/multiDiffEditor/colors.ts b/src/vs/editor/browser/widget/multiDiffEditor/colors.ts index d58781aabfe..297e5e86465 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/colors.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/colors.ts @@ -14,7 +14,7 @@ export const multiDiffEditorHeaderBackground = registerColor( export const multiDiffEditorBackground = registerColor( 'multiDiffEditor.background', - { dark: 'editorBackground', light: 'editorBackground', hcDark: 'editorBackground', hcLight: 'editorBackground', }, + 'editorBackground', localize('multiDiffEditor.background', 'The background color of the multi file diff editor') ); diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts index a044eb438ed..b58d7303e66 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts @@ -69,8 +69,9 @@ export class DocumentDiffItemViewModel extends Disposable { { contentHeight: 500, selections: undefined, } ); - public get originalUri(): URI | undefined { return this.entry.value!.original?.uri; } - public get modifiedUri(): URI | undefined { return this.entry.value!.modified?.uri; } + public get documentDiffItem(): IDocumentDiffItem { return this.entry.value!; } + public get originalUri(): URI | undefined { return this.documentDiffItem.original?.uri; } + public get modifiedUri(): URI | undefined { return this.documentDiffItem.modified?.uri; } public readonly isActive: IObservable = derived(this, reader => this._editorViewModel.activeDiffItem.read(reader) === this); diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts index 496b002489b..8275c4f7345 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts @@ -6,8 +6,8 @@ import { Dimension } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; import { derived, derivedWithStore, observableValue, recomputeInitiallyAndOnChange } from 'vs/base/common/observable'; -import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditor/utils'; -import { IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditor/model'; +import { readHotReloadableExport } from 'vs/base/common/hotReloadHelpers'; +import { IDocumentDiffItem, IMultiDiffEditorModel } from 'vs/editor/browser/widget/multiDiffEditor/model'; import { IMultiDiffEditorViewState, IMultiDiffResourceId, MultiDiffEditorWidgetImpl } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl'; import { MultiDiffEditorViewModel } from './multiDiffEditorViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -81,6 +81,10 @@ export class MultiDiffEditorWidget extends Disposable { public tryGetCodeEditor(resource: URI): { diffEditor: IDiffEditor; editor: ICodeEditor } | undefined { return this._widgetImpl.get().tryGetCodeEditor(resource); } + + public findDocumentDiffItem(resource: URI): IDocumentDiffItem | undefined { + return this._widgetImpl.get().findDocumentDiffItem(resource); + } } export interface RevealOptions { diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index c29fc74bddd..96cccc0afb8 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -31,6 +31,7 @@ import { DiffEditorItemTemplate, TemplateData } from './diffEditorItemTemplate'; import { DocumentDiffItemViewModel, MultiDiffEditorViewModel } from './multiDiffEditorViewModel'; import { ObjectPool } from './objectPool'; import { localize } from 'vs/nls'; +import { IDocumentDiffItem } from 'vs/editor/browser/widget/multiDiffEditor/model'; export class MultiDiffEditorWidgetImpl extends Disposable { private readonly _scrollableElements = h('div.scrollContent', [ @@ -73,8 +74,8 @@ export class MultiDiffEditorWidgetImpl extends Disposable { return template; })); - public readonly scrollTop = observableFromEvent(this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop); - public readonly scrollLeft = observableFromEvent(this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft); + public readonly scrollTop = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollTop */ this._scrollableElement.getScrollPosition().scrollTop); + public readonly scrollLeft = observableFromEvent(this, this._scrollableElement.onScroll, () => /** @description scrollLeft */ this._scrollableElement.getScrollPosition().scrollLeft); private readonly _viewItemsInfo = derivedWithStore<{ items: readonly VirtualizedViewItem[]; getItem: (viewModel: DocumentDiffItemViewModel) => VirtualizedViewItem }>(this, (reader, store) => { @@ -263,6 +264,14 @@ export class MultiDiffEditorWidgetImpl extends Disposable { }); } + public findDocumentDiffItem(resource: URI): IDocumentDiffItem | undefined { + const item = this._viewItems.get().find(v => + v.viewModel.diffEditorViewModel.model.modified.uri.toString() === resource.toString() + || v.viewModel.diffEditorViewModel.model.original.uri.toString() === resource.toString() + ); + return item?.viewModel.documentDiffItem; + } + public tryGetCodeEditor(resource: URI): { diffEditor: IDiffEditor; editor: ICodeEditor } | undefined { const item = this._viewItems.get().find(v => v.viewModel.diffEditorViewModel.model.modified.uri.toString() === resource.toString() @@ -272,6 +281,7 @@ export class MultiDiffEditorWidgetImpl extends Disposable { if (!editor) { return undefined; } + if (item.viewModel.diffEditorViewModel.model.modified.uri.toString() === resource.toString()) { return { diffEditor: editor, editor: editor.getModifiedEditor() }; } else { diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index fab223456fe..64394443765 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -692,6 +692,13 @@ export interface IEditorOptions { * Defaults to false. */ peekWidgetDefaultFocus?: 'tree' | 'editor'; + + /** + * Sets a placeholder for the editor. + * If set, the placeholder is shown if the editor is empty. + */ + placeholder?: string | undefined; + /** * Controls whether the definition link opens element in the peek widget. * Defaults to false. @@ -3071,6 +3078,10 @@ export interface IEditorMinimapOptions { * Font size of section headers. Defaults to 9. */ sectionHeaderFontSize?: number; + /** + * Spacing between the section header characters (in CSS px). Defaults to 1. + */ + sectionHeaderLetterSpacing?: number; } /** @@ -3093,6 +3104,7 @@ class EditorMinimap extends BaseEditorOption { + constructor() { + super(EditorOption.placeholder, 'placeholder', undefined); + } + + public validate(input: any): string | undefined { + if (typeof input === 'undefined') { + return this.defaultValue; + } + if (typeof input === 'string') { + return input; + } + return this.defaultValue; + } +} +//#endregion + //#region quickSuggestions export type QuickSuggestionsValue = 'on' | 'inline' | 'off'; @@ -3394,7 +3431,7 @@ class EditorQuickSuggestions extends BaseEditorOption lines[lineNumber - 1], + lines.length + ); + } +} + export class StringText extends AbstractText { private readonly _t = new PositionOffsetTransformer(this.value); diff --git a/src/vs/editor/common/cursor/cursor.ts b/src/vs/editor/common/cursor/cursor.ts index 4df16ec8db2..f84b5135d13 100644 --- a/src/vs/editor/common/cursor/cursor.ts +++ b/src/vs/editor/common/cursor/cursor.ts @@ -10,7 +10,8 @@ import { CursorConfiguration, CursorState, EditOperationResult, EditOperationTyp import { CursorContext } from 'vs/editor/common/cursor/cursorContext'; import { DeleteOperations } from 'vs/editor/common/cursor/cursorDeleteOperations'; import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; -import { CompositionOutcome, TypeOperations, TypeWithAutoClosingCommand } from 'vs/editor/common/cursor/cursorTypeOperations'; +import { CompositionOutcome, TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations'; +import { BaseTypeWithAutoClosingCommand } from 'vs/editor/common/cursor/cursorTypeEditOperations'; import { Position } from 'vs/editor/common/core/position'; import { Range, IRange } from 'vs/editor/common/core/range'; import { ISelection, Selection, SelectionDirection } from 'vs/editor/common/core/selection'; @@ -367,7 +368,7 @@ export class CursorsController extends Disposable { for (let i = 0; i < opResult.commands.length; i++) { const command = opResult.commands[i]; - if (command instanceof TypeWithAutoClosingCommand && command.enclosingRange && command.closeCharacterRange) { + if (command instanceof BaseTypeWithAutoClosingCommand && command.enclosingRange && command.closeCharacterRange) { autoClosedCharactersRanges.push(command.closeCharacterRange); autoClosedEnclosingRanges.push(command.enclosingRange); } diff --git a/src/vs/editor/common/cursor/cursorTypeEditOperations.ts b/src/vs/editor/common/cursor/cursorTypeEditOperations.ts new file mode 100644 index 00000000000..df17d2f3918 --- /dev/null +++ b/src/vs/editor/common/cursor/cursorTypeEditOperations.ts @@ -0,0 +1,1030 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CharCode } from 'vs/base/common/charCode'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import * as strings from 'vs/base/common/strings'; +import { ReplaceCommand, ReplaceCommandWithOffsetCursorState, ReplaceCommandWithoutChangingPosition, ReplaceCommandThatPreservesSelection } from 'vs/editor/common/commands/replaceCommand'; +import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; +import { SurroundSelectionCommand } from 'vs/editor/common/commands/surroundSelectionCommand'; +import { CursorConfiguration, EditOperationResult, EditOperationType, ICursorSimpleModel, isQuote } from 'vs/editor/common/cursorCommon'; +import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/core/wordCharacterClassifier'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { Position } from 'vs/editor/common/core/position'; +import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; +import { EnterAction, IndentAction, StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; +import { getIndentationAtPosition } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IElectricAction } from 'vs/editor/common/languages/supports/electricCharacter'; +import { EditorAutoClosingStrategy, EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; +import { createScopedLineTokens } from 'vs/editor/common/languages/supports'; +import { getIndentActionForType, getIndentForEnter, getInheritIndentForLine } from 'vs/editor/common/languages/autoIndent'; +import { getEnterAction } from 'vs/editor/common/languages/enterAction'; + +export class AutoIndentOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { + if (!isDoingComposition && this._isAutoIndentType(config, model, selections)) { + const indentationForSelections: { selection: Selection; indentation: string }[] = []; + for (const selection of selections) { + const indentation = this._findActualIndentationForSelection(config, model, selection, ch); + if (indentation === null) { + // Auto indentation failed + return; + } + indentationForSelections.push({ selection, indentation }); + } + const autoClosingPairClose = AutoClosingOpenCharTypeOperation.getAutoClosingPairClose(config, model, selections, ch, false); + return this._getIndentationAndAutoClosingPairEdits(config, model, indentationForSelections, ch, autoClosingPairClose); + } + return; + } + + private static _isAutoIndentType(config: CursorConfiguration, model: ITextModel, selections: Selection[]): boolean { + if (config.autoIndent < EditorAutoIndentStrategy.Full) { + return false; + } + for (let i = 0, len = selections.length; i < len; i++) { + if (!model.tokenization.isCheapToTokenize(selections[i].getEndPosition().lineNumber)) { + return false; + } + } + return true; + } + + private static _findActualIndentationForSelection(config: CursorConfiguration, model: ITextModel, selection: Selection, ch: string): string | null { + const actualIndentation = getIndentActionForType(config, model, selection, ch, { + shiftIndent: (indentation) => { + return shiftIndent(config, indentation); + }, + unshiftIndent: (indentation) => { + return unshiftIndent(config, indentation); + }, + }, config.languageConfigurationService); + + if (actualIndentation === null) { + return null; + } + + const currentIndentation = getIndentationAtPosition(model, selection.startLineNumber, selection.startColumn); + if (actualIndentation === config.normalizeIndentation(currentIndentation)) { + return null; + } + return actualIndentation; + } + + private static _getIndentationAndAutoClosingPairEdits(config: CursorConfiguration, model: ITextModel, indentationForSelections: { selection: Selection; indentation: string }[], ch: string, autoClosingPairClose: string | null): EditOperationResult { + const commands: ICommand[] = indentationForSelections.map(({ selection, indentation }) => { + if (autoClosingPairClose !== null) { + // Apply both auto closing pair edits and auto indentation edits + const indentationEdit = this._getEditFromIndentationAndSelection(config, model, indentation, selection, ch, false); + return new TypeWithIndentationAndAutoClosingCommand(indentationEdit, selection, ch, autoClosingPairClose); + } else { + // Apply only auto indentation edits + const indentationEdit = this._getEditFromIndentationAndSelection(config, model, indentation, selection, ch, true); + return typeCommand(indentationEdit.range, indentationEdit.text, false); + } + }); + const editOptions = { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false }; + return new EditOperationResult(EditOperationType.TypingOther, commands, editOptions); + } + + private static _getEditFromIndentationAndSelection(config: CursorConfiguration, model: ITextModel, indentation: string, selection: Selection, ch: string, includeChInEdit: boolean = true): { range: Range; text: string } { + const startLineNumber = selection.startLineNumber; + const firstNonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(startLineNumber); + let text: string = config.normalizeIndentation(indentation); + if (firstNonWhitespaceColumn !== 0) { + const startLine = model.getLineContent(startLineNumber); + text += startLine.substring(firstNonWhitespaceColumn - 1, selection.startColumn - 1); + } + text += includeChInEdit ? ch : ''; + const range = new Range(startLineNumber, 1, selection.endLineNumber, selection.endColumn); + return { range, text }; + } +} + +export class AutoClosingOvertypeOperation { + + public static getEdits(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult | undefined { + if (isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { + return this._runAutoClosingOvertype(prevEditOperationType, selections, ch); + } + return; + } + + private static _runAutoClosingOvertype(prevEditOperationType: EditOperationType, selections: Selection[], ch: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + const position = selection.getPosition(); + const typeSelection = new Range(position.lineNumber, position.column, position.lineNumber, position.column + 1); + commands[i] = new ReplaceCommand(typeSelection, ch); + } + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), + shouldPushStackElementAfter: false + }); + } +} + +export class AutoClosingOvertypeWithInterceptorsOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult | undefined { + if (isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { + // Unfortunately, the close character is at this point "doubled", so we need to delete it... + const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false)); + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: false + }); + } + return; + } +} + +export class AutoClosingOpenCharTypeOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean, isDoingComposition: boolean): EditOperationResult | undefined { + if (!isDoingComposition) { + const autoClosingPairClose = this.getAutoClosingPairClose(config, model, selections, ch, chIsAlreadyTyped); + if (autoClosingPairClose !== null) { + return this._runAutoClosingOpenCharType(selections, ch, chIsAlreadyTyped, autoClosingPairClose); + } + } + return; + } + + private static _runAutoClosingOpenCharType(selections: Selection[], ch: string, chIsAlreadyTyped: boolean, autoClosingPairClose: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + commands[i] = new TypeWithAutoClosingCommand(selection, ch, !chIsAlreadyTyped, autoClosingPairClose); + } + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: false + }); + } + + public static getAutoClosingPairClose(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean): string | null { + for (const selection of selections) { + if (!selection.isEmpty()) { + return null; + } + } + // This method is called both when typing (regularly) and when composition ends + // This means that we need to work with a text buffer where sometimes `ch` is not + // there (it is being typed right now) or with a text buffer where `ch` has already been typed + // + // In order to avoid adding checks for `chIsAlreadyTyped` in all places, we will work + // with two conceptual positions, the position before `ch` and the position after `ch` + // + const positions: { lineNumber: number; beforeColumn: number; afterColumn: number }[] = selections.map((s) => { + const position = s.getPosition(); + if (chIsAlreadyTyped) { + return { lineNumber: position.lineNumber, beforeColumn: position.column - ch.length, afterColumn: position.column }; + } else { + return { lineNumber: position.lineNumber, beforeColumn: position.column, afterColumn: position.column }; + } + }); + // Find the longest auto-closing open pair in case of multiple ending in `ch` + // e.g. when having [f","] and [","], it picks [f","] if the character before is f + const pair = this._findAutoClosingPairOpen(config, model, positions.map(p => new Position(p.lineNumber, p.beforeColumn)), ch); + if (!pair) { + return null; + } + let autoCloseConfig: EditorAutoClosingStrategy; + let shouldAutoCloseBefore: (ch: string) => boolean; + + const chIsQuote = isQuote(ch); + if (chIsQuote) { + autoCloseConfig = config.autoClosingQuotes; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.quote; + } else { + const pairIsForComments = config.blockCommentStartToken ? pair.open.includes(config.blockCommentStartToken) : false; + if (pairIsForComments) { + autoCloseConfig = config.autoClosingComments; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.comment; + } else { + autoCloseConfig = config.autoClosingBrackets; + shouldAutoCloseBefore = config.shouldAutoCloseBefore.bracket; + } + } + if (autoCloseConfig === 'never') { + return null; + } + // Sometimes, it is possible to have two auto-closing pairs that have a containment relationship + // e.g. when having [(,)] and [(*,*)] + // - when typing (, the resulting state is (|) + // - when typing *, the desired resulting state is (*|*), not (*|*)) + const containedPair = this._findContainedAutoClosingPair(config, pair); + const containedPairClose = containedPair ? containedPair.close : ''; + let isContainedPairPresent = true; + + for (const position of positions) { + const { lineNumber, beforeColumn, afterColumn } = position; + const lineText = model.getLineContent(lineNumber); + const lineBefore = lineText.substring(0, beforeColumn - 1); + const lineAfter = lineText.substring(afterColumn - 1); + + if (!lineAfter.startsWith(containedPairClose)) { + isContainedPairPresent = false; + } + // Only consider auto closing the pair if an allowed character follows or if another autoclosed pair closing brace follows + if (lineAfter.length > 0) { + const characterAfter = lineAfter.charAt(0); + const isBeforeCloseBrace = this._isBeforeClosingBrace(config, lineAfter); + if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) { + return null; + } + } + // Do not auto-close ' or " after a word character + if (pair.open.length === 1 && (ch === '\'' || ch === '"') && autoCloseConfig !== 'always') { + const wordSeparators = getMapForWordSeparators(config.wordSeparators, []); + if (lineBefore.length > 0) { + const characterBefore = lineBefore.charCodeAt(lineBefore.length - 1); + if (wordSeparators.get(characterBefore) === WordCharacterClass.Regular) { + return null; + } + } + } + if (!model.tokenization.isCheapToTokenize(lineNumber)) { + // Do not force tokenization + return null; + } + model.tokenization.forceTokenization(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, beforeColumn - 1); + if (!pair.shouldAutoClose(scopedLineTokens, beforeColumn - scopedLineTokens.firstCharOffset)) { + return null; + } + // Typing for example a quote could either start a new string, in which case auto-closing is desirable + // or it could end a previously started string, in which case auto-closing is not desirable + // + // In certain cases, it is really not possible to look at the previous token to determine + // what would happen. That's why we do something really unusual, we pretend to type a different + // character and ask the tokenizer what the outcome of doing that is: after typing a neutral + // character, are we in a string (i.e. the quote would most likely end a string) or not? + // + const neutralCharacter = pair.findNeutralCharacter(); + if (neutralCharacter) { + const tokenType = model.tokenization.getTokenTypeIfInsertingCharacter(lineNumber, beforeColumn, neutralCharacter); + if (!pair.isOK(tokenType)) { + return null; + } + } + } + if (isContainedPairPresent) { + return pair.close.substring(0, pair.close.length - containedPairClose.length); + } else { + return pair.close; + } + } + + /** + * Find another auto-closing pair that is contained by the one passed in. + * + * e.g. when having [(,)] and [(*,*)] as auto-closing pairs + * this method will find [(,)] as a containment pair for [(*,*)] + */ + private static _findContainedAutoClosingPair(config: CursorConfiguration, pair: StandardAutoClosingPairConditional): StandardAutoClosingPairConditional | null { + if (pair.open.length <= 1) { + return null; + } + const lastChar = pair.close.charAt(pair.close.length - 1); + // get candidates with the same last character as close + const candidates = config.autoClosingPairs.autoClosingPairsCloseByEnd.get(lastChar) || []; + let result: StandardAutoClosingPairConditional | null = null; + for (const candidate of candidates) { + if (candidate.open !== pair.open && pair.open.includes(candidate.open) && pair.close.endsWith(candidate.close)) { + if (!result || candidate.open.length > result.open.length) { + result = candidate; + } + } + } + return result; + } + + /** + * Determine if typing `ch` at all `positions` in the `model` results in an + * auto closing open sequence being typed. + * + * Auto closing open sequences can consist of multiple characters, which + * can lead to ambiguities. In such a case, the longest auto-closing open + * sequence is returned. + */ + private static _findAutoClosingPairOpen(config: CursorConfiguration, model: ITextModel, positions: Position[], ch: string): StandardAutoClosingPairConditional | null { + const candidates = config.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch); + if (!candidates) { + return null; + } + // Determine which auto-closing pair it is + let result: StandardAutoClosingPairConditional | null = null; + for (const candidate of candidates) { + if (result === null || candidate.open.length > result.open.length) { + let candidateIsMatch = true; + for (const position of positions) { + const relevantText = model.getValueInRange(new Range(position.lineNumber, position.column - candidate.open.length + 1, position.lineNumber, position.column)); + if (relevantText + ch !== candidate.open) { + candidateIsMatch = false; + break; + } + } + if (candidateIsMatch) { + result = candidate; + } + } + } + return result; + } + + private static _isBeforeClosingBrace(config: CursorConfiguration, lineAfter: string) { + // If the start of lineAfter can be interpretted as both a starting or ending brace, default to returning false + const nextChar = lineAfter.charAt(0); + const potentialStartingBraces = config.autoClosingPairs.autoClosingPairsOpenByStart.get(nextChar) || []; + const potentialClosingBraces = config.autoClosingPairs.autoClosingPairsCloseByStart.get(nextChar) || []; + + const isBeforeStartingBrace = potentialStartingBraces.some(x => lineAfter.startsWith(x.open)); + const isBeforeClosingBrace = potentialClosingBraces.some(x => lineAfter.startsWith(x.close)); + + return !isBeforeStartingBrace && isBeforeClosingBrace; + } +} + +export class SurroundSelectionOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { + if (!isDoingComposition && this._isSurroundSelectionType(config, model, selections, ch)) { + return this._runSurroundSelectionType(config, selections, ch); + } + return; + } + + private static _runSurroundSelectionType(config: CursorConfiguration, selections: Selection[], ch: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + const closeCharacter = config.surroundingPairs[ch]; + commands[i] = new SurroundSelectionCommand(selection, ch, closeCharacter); + } + return new EditOperationResult(EditOperationType.Other, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: true + }); + } + + private static _isSurroundSelectionType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean { + if (!shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { + return false; + } + const isTypingAQuoteCharacter = isQuote(ch); + for (const selection of selections) { + if (selection.isEmpty()) { + return false; + } + let selectionContainsOnlyWhitespace = true; + for (let lineNumber = selection.startLineNumber; lineNumber <= selection.endLineNumber; lineNumber++) { + const lineText = model.getLineContent(lineNumber); + const startIndex = (lineNumber === selection.startLineNumber ? selection.startColumn - 1 : 0); + const endIndex = (lineNumber === selection.endLineNumber ? selection.endColumn - 1 : lineText.length); + const selectedText = lineText.substring(startIndex, endIndex); + if (/[^ \t]/.test(selectedText)) { + // this selected text contains something other than whitespace + selectionContainsOnlyWhitespace = false; + break; + } + } + if (selectionContainsOnlyWhitespace) { + return false; + } + if (isTypingAQuoteCharacter && selection.startLineNumber === selection.endLineNumber && selection.startColumn + 1 === selection.endColumn) { + const selectionText = model.getValueInRange(selection); + if (isQuote(selectionText)) { + // Typing a quote character on top of another quote character + // => disable surround selection type + return false; + } + } + } + return true; + } +} + +export class InterceptorElectricCharOperation { + + public static getEdits(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { + // Electric characters make sense only when dealing with a single cursor, + // as multiple cursors typing brackets for example would interfer with bracket matching + if (!isDoingComposition && this._isTypeInterceptorElectricChar(config, model, selections)) { + const r = this._typeInterceptorElectricChar(prevEditOperationType, config, model, selections[0], ch); + if (r) { + return r; + } + } + return; + } + + private static _isTypeInterceptorElectricChar(config: CursorConfiguration, model: ITextModel, selections: Selection[]) { + if (selections.length === 1 && model.tokenization.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) { + return true; + } + return false; + } + + private static _typeInterceptorElectricChar(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selection: Selection, ch: string): EditOperationResult | null { + if (!config.electricChars.hasOwnProperty(ch) || !selection.isEmpty()) { + return null; + } + const position = selection.getPosition(); + model.tokenization.forceTokenization(position.lineNumber); + const lineTokens = model.tokenization.getLineTokens(position.lineNumber); + let electricAction: IElectricAction | null; + try { + electricAction = config.onElectricCharacter(ch, lineTokens, position.column); + } catch (e) { + onUnexpectedError(e); + return null; + } + if (!electricAction) { + return null; + } + if (electricAction.matchOpenBracket) { + const endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1; + const match = model.bracketPairs.findMatchingBracketUp(electricAction.matchOpenBracket, { + lineNumber: position.lineNumber, + column: endColumn + }, 500 /* give at most 500ms to compute */); + if (match) { + if (match.startLineNumber === position.lineNumber) { + // matched something on the same line => no change in indentation + return null; + } + const matchLine = model.getLineContent(match.startLineNumber); + const matchLineIndentation = strings.getLeadingWhitespace(matchLine); + const newIndentation = config.normalizeIndentation(matchLineIndentation); + const lineText = model.getLineContent(position.lineNumber); + const lineFirstNonBlankColumn = model.getLineFirstNonWhitespaceColumn(position.lineNumber) || position.column; + const prefix = lineText.substring(lineFirstNonBlankColumn - 1, position.column - 1); + const typeText = newIndentation + prefix + ch; + const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, position.column); + const command = new ReplaceCommand(typeSelection, typeText); + return new EditOperationResult(getTypingOperation(typeText, prevEditOperationType), [command], { + shouldPushStackElementBefore: false, + shouldPushStackElementAfter: true + }); + } + } + return null; + } +} + +export class SimpleCharacterTypeOperation { + + public static getEdits(prevEditOperationType: EditOperationType, selections: Selection[], ch: string): EditOperationResult { + // A simple character type + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = new ReplaceCommand(selections[i], ch); + } + + const opType = getTypingOperation(ch, prevEditOperationType); + return new EditOperationResult(opType, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), + shouldPushStackElementAfter: false + }); + } +} + +export class EnterOperation { + + public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined { + if (!isDoingComposition && ch === '\n') { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = this._enter(config, model, false, selections[i]); + } + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: false, + }); + } + return; + } + + private static _enter(config: CursorConfiguration, model: ITextModel, keepPosition: boolean, range: Range): ICommand { + if (config.autoIndent === EditorAutoIndentStrategy.None) { + return typeCommand(range, '\n', keepPosition); + } + if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { + const lineText = model.getLineContent(range.startLineNumber); + const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); + return typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); + } + const r = getEnterAction(config.autoIndent, model, range, config.languageConfigurationService); + if (r) { + if (r.indentAction === IndentAction.None) { + // Nothing special + return typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); + + } else if (r.indentAction === IndentAction.Indent) { + // Indent once + return typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); + + } else if (r.indentAction === IndentAction.IndentOutdent) { + // Ultra special + const normalIndent = config.normalizeIndentation(r.indentation); + const increasedIndent = config.normalizeIndentation(r.indentation + r.appendText); + const typeText = '\n' + increasedIndent + '\n' + normalIndent; + if (keepPosition) { + return new ReplaceCommandWithoutChangingPosition(range, typeText, true); + } else { + return new ReplaceCommandWithOffsetCursorState(range, typeText, -1, increasedIndent.length - normalIndent.length, true); + } + } else if (r.indentAction === IndentAction.Outdent) { + const actualIndentation = unshiftIndent(config, r.indentation); + return typeCommand(range, '\n' + config.normalizeIndentation(actualIndentation + r.appendText), keepPosition); + } + } + + const lineText = model.getLineContent(range.startLineNumber); + const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); + + if (config.autoIndent >= EditorAutoIndentStrategy.Full) { + const ir = getIndentForEnter(config.autoIndent, model, range, { + unshiftIndent: (indent) => { + return unshiftIndent(config, indent); + }, + shiftIndent: (indent) => { + return shiftIndent(config, indent); + }, + normalizeIndentation: (indent) => { + return config.normalizeIndentation(indent); + } + }, config.languageConfigurationService); + + if (ir) { + let oldEndViewColumn = config.visibleColumnFromColumn(model, range.getEndPosition()); + const oldEndColumn = range.endColumn; + const newLineContent = model.getLineContent(range.endLineNumber); + const firstNonWhitespace = strings.firstNonWhitespaceIndex(newLineContent); + if (firstNonWhitespace >= 0) { + range = range.setEndPosition(range.endLineNumber, Math.max(range.endColumn, firstNonWhitespace + 1)); + } else { + range = range.setEndPosition(range.endLineNumber, model.getLineMaxColumn(range.endLineNumber)); + } + if (keepPosition) { + return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); + } else { + let offset = 0; + if (oldEndColumn <= firstNonWhitespace + 1) { + if (!config.insertSpaces) { + oldEndViewColumn = Math.ceil(oldEndViewColumn / config.indentSize); + } + offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); + } + return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); + } + } + } + return typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); + } + + + public static lineInsertBefore(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { + if (model === null || selections === null) { + return []; + } + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + let lineNumber = selections[i].positionLineNumber; + if (lineNumber === 1) { + commands[i] = new ReplaceCommandWithoutChangingPosition(new Range(1, 1, 1, 1), '\n'); + } else { + lineNumber--; + const column = model.getLineMaxColumn(lineNumber); + + commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); + } + } + return commands; + } + + public static lineInsertAfter(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { + if (model === null || selections === null) { + return []; + } + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const lineNumber = selections[i].positionLineNumber; + const column = model.getLineMaxColumn(lineNumber); + commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); + } + return commands; + } + + public static lineBreakInsert(config: CursorConfiguration, model: ITextModel, selections: Selection[]): ICommand[] { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = this._enter(config, model, true, selections[i]); + } + return commands; + } +} + +export class PasteOperation { + + public static getEdits(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]) { + const distributedPaste = this._distributePasteToCursors(config, selections, text, pasteOnNewLine, multicursorText); + if (distributedPaste) { + selections = selections.sort(Range.compareRangesUsingStarts); + return this._distributedPaste(config, model, selections, distributedPaste); + } else { + return this._simplePaste(config, model, selections, text, pasteOnNewLine); + } + } + + private static _distributePasteToCursors(config: CursorConfiguration, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]): string[] | null { + if (pasteOnNewLine) { + return null; + } + if (selections.length === 1) { + return null; + } + if (multicursorText && multicursorText.length === selections.length) { + return multicursorText; + } + if (config.multiCursorPaste === 'spread') { + // Try to spread the pasted text in case the line count matches the cursor count + // Remove trailing \n if present + if (text.charCodeAt(text.length - 1) === CharCode.LineFeed) { + text = text.substring(0, text.length - 1); + } + // Remove trailing \r if present + if (text.charCodeAt(text.length - 1) === CharCode.CarriageReturn) { + text = text.substring(0, text.length - 1); + } + const lines = strings.splitLines(text); + if (lines.length === selections.length) { + return lines; + } + } + return null; + } + + private static _distributedPaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string[]): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = new ReplaceCommand(selections[i], text[i]); + } + return new EditOperationResult(EditOperationType.Other, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: true + }); + } + + private static _simplePaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + const position = selection.getPosition(); + if (pasteOnNewLine && !selection.isEmpty()) { + pasteOnNewLine = false; + } + if (pasteOnNewLine && text.indexOf('\n') !== text.length - 1) { + pasteOnNewLine = false; + } + if (pasteOnNewLine) { + // Paste entire line at the beginning of line + const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, 1); + commands[i] = new ReplaceCommandThatPreservesSelection(typeSelection, text, selection, true); + } else { + commands[i] = new ReplaceCommand(selection, text); + } + } + return new EditOperationResult(EditOperationType.Other, commands, { + shouldPushStackElementBefore: true, + shouldPushStackElementAfter: true + }); + } +} + +export class CompositionOperation { + + public static getEdits(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number) { + const commands = selections.map(selection => this._compositionType(model, selection, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta)); + return new EditOperationResult(EditOperationType.TypingOther, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), + shouldPushStackElementAfter: false + }); + } + + private static _compositionType(model: ITextModel, selection: Selection, text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): ICommand | null { + if (!selection.isEmpty()) { + // looks like https://github.com/microsoft/vscode/issues/2773 + // where a cursor operation occurred before a canceled composition + // => ignore composition + return null; + } + const pos = selection.getPosition(); + const startColumn = Math.max(1, pos.column - replacePrevCharCnt); + const endColumn = Math.min(model.getLineMaxColumn(pos.lineNumber), pos.column + replaceNextCharCnt); + const range = new Range(pos.lineNumber, startColumn, pos.lineNumber, endColumn); + const oldText = model.getValueInRange(range); + if (oldText === text && positionDelta === 0) { + // => ignore composition that doesn't do anything + return null; + } + return new ReplaceCommandWithOffsetCursorState(range, text, 0, positionDelta); + } +} + +export class TypeWithoutInterceptorsOperation { + + public static getEdits(prevEditOperationType: EditOperationType, selections: Selection[], str: string): EditOperationResult { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + commands[i] = new ReplaceCommand(selections[i], str); + } + const opType = getTypingOperation(str, prevEditOperationType); + return new EditOperationResult(opType, commands, { + shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), + shouldPushStackElementAfter: false + }); + } +} + +export class TabOperation { + + public static getCommands(config: CursorConfiguration, model: ITextModel, selections: Selection[]) { + const commands: ICommand[] = []; + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + if (selection.isEmpty()) { + const lineText = model.getLineContent(selection.startLineNumber); + if (/^\s*$/.test(lineText) && model.tokenization.isCheapToTokenize(selection.startLineNumber)) { + let goodIndent = this._goodIndentForLine(config, model, selection.startLineNumber); + goodIndent = goodIndent || '\t'; + const possibleTypeText = config.normalizeIndentation(goodIndent); + if (!lineText.startsWith(possibleTypeText)) { + commands[i] = new ReplaceCommand(new Range(selection.startLineNumber, 1, selection.startLineNumber, lineText.length + 1), possibleTypeText, true); + continue; + } + } + commands[i] = this._replaceJumpToNextIndent(config, model, selection, true); + } else { + if (selection.startLineNumber === selection.endLineNumber) { + const lineMaxColumn = model.getLineMaxColumn(selection.startLineNumber); + if (selection.startColumn !== 1 || selection.endColumn !== lineMaxColumn) { + // This is a single line selection that is not the entire line + commands[i] = this._replaceJumpToNextIndent(config, model, selection, false); + continue; + } + } + commands[i] = new ShiftCommand(selection, { + isUnshift: false, + tabSize: config.tabSize, + indentSize: config.indentSize, + insertSpaces: config.insertSpaces, + useTabStops: config.useTabStops, + autoIndent: config.autoIndent + }, config.languageConfigurationService); + } + } + return commands; + } + + private static _goodIndentForLine(config: CursorConfiguration, model: ITextModel, lineNumber: number): string | null { + let action: IndentAction | EnterAction | null = null; + let indentation: string = ''; + const expectedIndentAction = getInheritIndentForLine(config.autoIndent, model, lineNumber, false, config.languageConfigurationService); + if (expectedIndentAction) { + action = expectedIndentAction.action; + indentation = expectedIndentAction.indentation; + } else if (lineNumber > 1) { + let lastLineNumber: number; + for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) { + const lineText = model.getLineContent(lastLineNumber); + const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineText); + if (nonWhitespaceIdx >= 0) { + break; + } + } + if (lastLineNumber < 1) { + // No previous line with content found + return null; + } + const maxColumn = model.getLineMaxColumn(lastLineNumber); + const expectedEnterAction = getEnterAction(config.autoIndent, model, new Range(lastLineNumber, maxColumn, lastLineNumber, maxColumn), config.languageConfigurationService); + if (expectedEnterAction) { + indentation = expectedEnterAction.indentation + expectedEnterAction.appendText; + } + } + if (action) { + if (action === IndentAction.Indent) { + indentation = shiftIndent(config, indentation); + } + if (action === IndentAction.Outdent) { + indentation = unshiftIndent(config, indentation); + } + indentation = config.normalizeIndentation(indentation); + } + if (!indentation) { + return null; + } + return indentation; + } + + private static _replaceJumpToNextIndent(config: CursorConfiguration, model: ICursorSimpleModel, selection: Selection, insertsAutoWhitespace: boolean): ReplaceCommand { + let typeText = ''; + const position = selection.getStartPosition(); + if (config.insertSpaces) { + const visibleColumnFromColumn = config.visibleColumnFromColumn(model, position); + const indentSize = config.indentSize; + const spacesCnt = indentSize - (visibleColumnFromColumn % indentSize); + for (let i = 0; i < spacesCnt; i++) { + typeText += ' '; + } + } else { + typeText = '\t'; + } + return new ReplaceCommand(selection, typeText, insertsAutoWhitespace); + } +} + +export class BaseTypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState { + + private readonly _openCharacter: string; + private readonly _closeCharacter: string; + public closeCharacterRange: Range | null; + public enclosingRange: Range | null; + + constructor(selection: Selection, text: string, lineNumberDeltaOffset: number, columnDeltaOffset: number, openCharacter: string, closeCharacter: string) { + super(selection, text, lineNumberDeltaOffset, columnDeltaOffset); + this._openCharacter = openCharacter; + this._closeCharacter = closeCharacter; + this.closeCharacterRange = null; + this.enclosingRange = null; + } + + protected _computeCursorStateWithRange(model: ITextModel, range: Range, helper: ICursorStateComputerData): Selection { + this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn); + this.enclosingRange = new Range(range.startLineNumber, range.endColumn - this._openCharacter.length - this._closeCharacter.length, range.endLineNumber, range.endColumn); + return super.computeCursorState(model, helper); + } +} + +class TypeWithAutoClosingCommand extends BaseTypeWithAutoClosingCommand { + + constructor(selection: Selection, openCharacter: string, insertOpenCharacter: boolean, closeCharacter: string) { + const text = (insertOpenCharacter ? openCharacter : '') + closeCharacter; + const lineNumberDeltaOffset = 0; + const columnDeltaOffset = -closeCharacter.length; + super(selection, text, lineNumberDeltaOffset, columnDeltaOffset, openCharacter, closeCharacter); + } + + public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { + const inverseEditOperations = helper.getInverseEditOperations(); + const range = inverseEditOperations[0].range; + return this._computeCursorStateWithRange(model, range, helper); + } +} + +class TypeWithIndentationAndAutoClosingCommand extends BaseTypeWithAutoClosingCommand { + + private readonly _autoIndentationEdit: { range: Range; text: string }; + private readonly _autoClosingEdit: { range: Range; text: string }; + + constructor(autoIndentationEdit: { range: Range; text: string }, selection: Selection, openCharacter: string, closeCharacter: string) { + const text = openCharacter + closeCharacter; + const lineNumberDeltaOffset = 0; + const columnDeltaOffset = openCharacter.length; + super(selection, text, lineNumberDeltaOffset, columnDeltaOffset, openCharacter, closeCharacter); + this._autoIndentationEdit = autoIndentationEdit; + this._autoClosingEdit = { range: selection, text }; + } + + public override getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { + builder.addTrackedEditOperation(this._autoIndentationEdit.range, this._autoIndentationEdit.text); + builder.addTrackedEditOperation(this._autoClosingEdit.range, this._autoClosingEdit.text); + } + + public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { + const inverseEditOperations = helper.getInverseEditOperations(); + if (inverseEditOperations.length !== 2) { + throw new Error('There should be two inverse edit operations!'); + } + const range1 = inverseEditOperations[0].range; + const range2 = inverseEditOperations[1].range; + const range = range1.plusRange(range2); + return this._computeCursorStateWithRange(model, range, helper); + } +} + +function getTypingOperation(typedText: string, previousTypingOperation: EditOperationType): EditOperationType { + if (typedText === ' ') { + return previousTypingOperation === EditOperationType.TypingFirstSpace + || previousTypingOperation === EditOperationType.TypingConsecutiveSpace + ? EditOperationType.TypingConsecutiveSpace + : EditOperationType.TypingFirstSpace; + } + + return EditOperationType.TypingOther; +} + +function shouldPushStackElementBetween(previousTypingOperation: EditOperationType, typingOperation: EditOperationType): boolean { + if (isTypingOperation(previousTypingOperation) && !isTypingOperation(typingOperation)) { + // Always set an undo stop before non-type operations + return true; + } + if (previousTypingOperation === EditOperationType.TypingFirstSpace) { + // `abc |d`: No undo stop + // `abc |d`: Undo stop + return false; + } + // Insert undo stop between different operation types + return normalizeOperationType(previousTypingOperation) !== normalizeOperationType(typingOperation); +} + +function normalizeOperationType(type: EditOperationType): EditOperationType | 'space' { + return (type === EditOperationType.TypingConsecutiveSpace || type === EditOperationType.TypingFirstSpace) + ? 'space' + : type; +} + +function isTypingOperation(type: EditOperationType): boolean { + return type === EditOperationType.TypingOther + || type === EditOperationType.TypingFirstSpace + || type === EditOperationType.TypingConsecutiveSpace; +} + +function isAutoClosingOvertype(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): boolean { + if (config.autoClosingOvertype === 'never') { + return false; + } + if (!config.autoClosingPairs.autoClosingPairsCloseSingleChar.has(ch)) { + return false; + } + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + if (!selection.isEmpty()) { + return false; + } + const position = selection.getPosition(); + const lineText = model.getLineContent(position.lineNumber); + const afterCharacter = lineText.charAt(position.column - 1); + if (afterCharacter !== ch) { + return false; + } + // Do not over-type quotes after a backslash + const chIsQuote = isQuote(ch); + const beforeCharacter = position.column > 2 ? lineText.charCodeAt(position.column - 2) : CharCode.Null; + if (beforeCharacter === CharCode.Backslash && chIsQuote) { + return false; + } + // Must over-type a closing character typed by the editor + if (config.autoClosingOvertype === 'auto') { + let found = false; + for (let j = 0, lenJ = autoClosedCharacters.length; j < lenJ; j++) { + const autoClosedCharacter = autoClosedCharacters[j]; + if (position.lineNumber === autoClosedCharacter.startLineNumber && position.column === autoClosedCharacter.startColumn) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + } + return true; +} + +function typeCommand(range: Range, text: string, keepPosition: boolean): ICommand { + if (keepPosition) { + return new ReplaceCommandWithoutChangingPosition(range, text, true); + } else { + return new ReplaceCommand(range, text, true); + } +} + +export function shiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { + count = count || 1; + return ShiftCommand.shiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); +} + +export function unshiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { + count = count || 1; + return ShiftCommand.unshiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); +} + +export function shouldSurroundChar(config: CursorConfiguration, ch: string): boolean { + if (isQuote(ch)) { + return (config.autoSurround === 'quotes' || config.autoSurround === 'languageDefined'); + } else { + // Character is a bracket + return (config.autoSurround === 'brackets' || config.autoSurround === 'languageDefined'); + } +} diff --git a/src/vs/editor/common/cursor/cursorTypeOperations.ts b/src/vs/editor/common/cursor/cursorTypeOperations.ts index ffa80cbb63c..b4c65156f85 100644 --- a/src/vs/editor/common/cursor/cursorTypeOperations.ts +++ b/src/vs/editor/common/cursor/cursorTypeOperations.ts @@ -3,26 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CharCode } from 'vs/base/common/charCode'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import * as strings from 'vs/base/common/strings'; -import { ReplaceCommand, ReplaceCommandWithOffsetCursorState, ReplaceCommandWithoutChangingPosition, ReplaceCommandThatPreservesSelection } from 'vs/editor/common/commands/replaceCommand'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; -import { CompositionSurroundSelectionCommand, SurroundSelectionCommand } from 'vs/editor/common/commands/surroundSelectionCommand'; +import { CompositionSurroundSelectionCommand } from 'vs/editor/common/commands/surroundSelectionCommand'; import { CursorConfiguration, EditOperationResult, EditOperationType, ICursorSimpleModel, isQuote } from 'vs/editor/common/cursorCommon'; -import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/core/wordCharacterClassifier'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { Position } from 'vs/editor/common/core/position'; -import { ICommand, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; +import { ICommand } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { EnterAction, IndentAction, StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; -import { getIndentationAtPosition } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { IElectricAction } from 'vs/editor/common/languages/supports/electricCharacter'; -import { EditorAutoClosingStrategy, EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; -import { createScopedLineTokens } from 'vs/editor/common/languages/supports'; -import { getIndentActionForType, getIndentForEnter, getInheritIndentForLine } from 'vs/editor/common/languages/autoIndent'; -import { getEnterAction } from 'vs/editor/common/languages/enterAction'; +import { AutoClosingOpenCharTypeOperation, AutoClosingOvertypeOperation, AutoClosingOvertypeWithInterceptorsOperation, AutoIndentOperation, CompositionOperation, EnterOperation, InterceptorElectricCharOperation, PasteOperation, shiftIndent, shouldSurroundChar, SimpleCharacterTypeOperation, SurroundSelectionOperation, TabOperation, TypeWithoutInterceptorsOperation, unshiftIndent } from 'vs/editor/common/cursor/cursorTypeEditOperations'; export class TypeOperations { @@ -61,777 +50,23 @@ export class TypeOperations { } public static shiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { - count = count || 1; - return ShiftCommand.shiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); + return shiftIndent(config, indentation, count); } public static unshiftIndent(config: CursorConfiguration, indentation: string, count?: number): string { - count = count || 1; - return ShiftCommand.unshiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces); - } - - private static _distributedPaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string[]): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = new ReplaceCommand(selections[i], text[i]); - } - return new EditOperationResult(EditOperationType.Other, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: true - }); - } - - private static _simplePaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - const position = selection.getPosition(); - - if (pasteOnNewLine && !selection.isEmpty()) { - pasteOnNewLine = false; - } - if (pasteOnNewLine && text.indexOf('\n') !== text.length - 1) { - pasteOnNewLine = false; - } - - if (pasteOnNewLine) { - // Paste entire line at the beginning of line - const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, 1); - commands[i] = new ReplaceCommandThatPreservesSelection(typeSelection, text, selection, true); - } else { - commands[i] = new ReplaceCommand(selection, text); - } - } - return new EditOperationResult(EditOperationType.Other, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: true - }); - } - - private static _distributePasteToCursors(config: CursorConfiguration, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]): string[] | null { - if (pasteOnNewLine) { - return null; - } - - if (selections.length === 1) { - return null; - } - - if (multicursorText && multicursorText.length === selections.length) { - return multicursorText; - } - - if (config.multiCursorPaste === 'spread') { - // Try to spread the pasted text in case the line count matches the cursor count - // Remove trailing \n if present - if (text.charCodeAt(text.length - 1) === CharCode.LineFeed) { - text = text.substr(0, text.length - 1); - } - // Remove trailing \r if present - if (text.charCodeAt(text.length - 1) === CharCode.CarriageReturn) { - text = text.substr(0, text.length - 1); - } - const lines = strings.splitLines(text); - if (lines.length === selections.length) { - return lines; - } - } - - return null; + return unshiftIndent(config, indentation, count); } public static paste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]): EditOperationResult { - const distributedPaste = this._distributePasteToCursors(config, selections, text, pasteOnNewLine, multicursorText); - - if (distributedPaste) { - selections = selections.sort(Range.compareRangesUsingStarts); - return this._distributedPaste(config, model, selections, distributedPaste); - } else { - return this._simplePaste(config, model, selections, text, pasteOnNewLine); - } - } - - private static _goodIndentForLine(config: CursorConfiguration, model: ITextModel, lineNumber: number): string | null { - let action: IndentAction | EnterAction | null = null; - let indentation: string = ''; - - const expectedIndentAction = getInheritIndentForLine(config.autoIndent, model, lineNumber, false, config.languageConfigurationService); - if (expectedIndentAction) { - action = expectedIndentAction.action; - indentation = expectedIndentAction.indentation; - } else if (lineNumber > 1) { - let lastLineNumber: number; - for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) { - const lineText = model.getLineContent(lastLineNumber); - const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineText); - if (nonWhitespaceIdx >= 0) { - break; - } - } - - if (lastLineNumber < 1) { - // No previous line with content found - return null; - } - - const maxColumn = model.getLineMaxColumn(lastLineNumber); - const expectedEnterAction = getEnterAction(config.autoIndent, model, new Range(lastLineNumber, maxColumn, lastLineNumber, maxColumn), config.languageConfigurationService); - if (expectedEnterAction) { - indentation = expectedEnterAction.indentation + expectedEnterAction.appendText; - } - } - - if (action) { - if (action === IndentAction.Indent) { - indentation = TypeOperations.shiftIndent(config, indentation); - } - - if (action === IndentAction.Outdent) { - indentation = TypeOperations.unshiftIndent(config, indentation); - } - - indentation = config.normalizeIndentation(indentation); - } - - if (!indentation) { - return null; - } - - return indentation; - } - - private static _replaceJumpToNextIndent(config: CursorConfiguration, model: ICursorSimpleModel, selection: Selection, insertsAutoWhitespace: boolean): ReplaceCommand { - let typeText = ''; - - const position = selection.getStartPosition(); - if (config.insertSpaces) { - const visibleColumnFromColumn = config.visibleColumnFromColumn(model, position); - const indentSize = config.indentSize; - const spacesCnt = indentSize - (visibleColumnFromColumn % indentSize); - for (let i = 0; i < spacesCnt; i++) { - typeText += ' '; - } - } else { - typeText = '\t'; - } - - return new ReplaceCommand(selection, typeText, insertsAutoWhitespace); + return PasteOperation.getEdits(config, model, selections, text, pasteOnNewLine, multicursorText); } public static tab(config: CursorConfiguration, model: ITextModel, selections: Selection[]): ICommand[] { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - - if (selection.isEmpty()) { - - const lineText = model.getLineContent(selection.startLineNumber); - - if (/^\s*$/.test(lineText) && model.tokenization.isCheapToTokenize(selection.startLineNumber)) { - let goodIndent = this._goodIndentForLine(config, model, selection.startLineNumber); - goodIndent = goodIndent || '\t'; - const possibleTypeText = config.normalizeIndentation(goodIndent); - if (!lineText.startsWith(possibleTypeText)) { - commands[i] = new ReplaceCommand(new Range(selection.startLineNumber, 1, selection.startLineNumber, lineText.length + 1), possibleTypeText, true); - continue; - } - } - - commands[i] = this._replaceJumpToNextIndent(config, model, selection, true); - } else { - if (selection.startLineNumber === selection.endLineNumber) { - const lineMaxColumn = model.getLineMaxColumn(selection.startLineNumber); - if (selection.startColumn !== 1 || selection.endColumn !== lineMaxColumn) { - // This is a single line selection that is not the entire line - commands[i] = this._replaceJumpToNextIndent(config, model, selection, false); - continue; - } - } - - commands[i] = new ShiftCommand(selection, { - isUnshift: false, - tabSize: config.tabSize, - indentSize: config.indentSize, - insertSpaces: config.insertSpaces, - useTabStops: config.useTabStops, - autoIndent: config.autoIndent - }, config.languageConfigurationService); - } - } - return commands; + return TabOperation.getCommands(config, model, selections); } public static compositionType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): EditOperationResult { - const commands = selections.map(selection => this._compositionType(model, selection, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta)); - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), - shouldPushStackElementAfter: false - }); - } - - private static _compositionType(model: ITextModel, selection: Selection, text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): ICommand | null { - if (!selection.isEmpty()) { - // looks like https://github.com/microsoft/vscode/issues/2773 - // where a cursor operation occurred before a canceled composition - // => ignore composition - return null; - } - const pos = selection.getPosition(); - const startColumn = Math.max(1, pos.column - replacePrevCharCnt); - const endColumn = Math.min(model.getLineMaxColumn(pos.lineNumber), pos.column + replaceNextCharCnt); - const range = new Range(pos.lineNumber, startColumn, pos.lineNumber, endColumn); - const oldText = model.getValueInRange(range); - if (oldText === text && positionDelta === 0) { - // => ignore composition that doesn't do anything - return null; - } - return new ReplaceCommandWithOffsetCursorState(range, text, 0, positionDelta); - } - - private static _typeCommand(range: Range, text: string, keepPosition: boolean): ICommand { - if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, text, true); - } else { - return new ReplaceCommand(range, text, true); - } - } - - private static _enter(config: CursorConfiguration, model: ITextModel, keepPosition: boolean, range: Range): ICommand { - if (config.autoIndent === EditorAutoIndentStrategy.None) { - return TypeOperations._typeCommand(range, '\n', keepPosition); - } - if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { - const lineText = model.getLineContent(range.startLineNumber); - const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); - } - - const r = getEnterAction(config.autoIndent, model, range, config.languageConfigurationService); - if (r) { - if (r.indentAction === IndentAction.None) { - // Nothing special - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); - - } else if (r.indentAction === IndentAction.Indent) { - // Indent once - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition); - - } else if (r.indentAction === IndentAction.IndentOutdent) { - // Ultra special - const normalIndent = config.normalizeIndentation(r.indentation); - const increasedIndent = config.normalizeIndentation(r.indentation + r.appendText); - - const typeText = '\n' + increasedIndent + '\n' + normalIndent; - - if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, typeText, true); - } else { - return new ReplaceCommandWithOffsetCursorState(range, typeText, -1, increasedIndent.length - normalIndent.length, true); - } - } else if (r.indentAction === IndentAction.Outdent) { - const actualIndentation = TypeOperations.unshiftIndent(config, r.indentation); - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(actualIndentation + r.appendText), keepPosition); - } - } - - const lineText = model.getLineContent(range.startLineNumber); - const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); - - if (config.autoIndent >= EditorAutoIndentStrategy.Full) { - const ir = getIndentForEnter(config.autoIndent, model, range, { - unshiftIndent: (indent) => { - return TypeOperations.unshiftIndent(config, indent); - }, - shiftIndent: (indent) => { - return TypeOperations.shiftIndent(config, indent); - }, - normalizeIndentation: (indent) => { - return config.normalizeIndentation(indent); - } - }, config.languageConfigurationService); - - if (ir) { - let oldEndViewColumn = config.visibleColumnFromColumn(model, range.getEndPosition()); - const oldEndColumn = range.endColumn; - const newLineContent = model.getLineContent(range.endLineNumber); - const firstNonWhitespace = strings.firstNonWhitespaceIndex(newLineContent); - if (firstNonWhitespace >= 0) { - range = range.setEndPosition(range.endLineNumber, Math.max(range.endColumn, firstNonWhitespace + 1)); - } else { - range = range.setEndPosition(range.endLineNumber, model.getLineMaxColumn(range.endLineNumber)); - } - - if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); - } else { - let offset = 0; - if (oldEndColumn <= firstNonWhitespace + 1) { - if (!config.insertSpaces) { - oldEndViewColumn = Math.ceil(oldEndViewColumn / config.indentSize); - } - offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); - } - return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); - } - } - } - - return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); - } - - private static _isAutoIndentType(config: CursorConfiguration, model: ITextModel, selections: Selection[]): boolean { - if (config.autoIndent < EditorAutoIndentStrategy.Full) { - return false; - } - - for (let i = 0, len = selections.length; i < len; i++) { - if (!model.tokenization.isCheapToTokenize(selections[i].getEndPosition().lineNumber)) { - return false; - } - } - - return true; - } - - private static _runAutoIndentType(config: CursorConfiguration, model: ITextModel, range: Range, ch: string): ICommand | null { - const currentIndentation = getIndentationAtPosition(model, range.startLineNumber, range.startColumn); - const actualIndentation = getIndentActionForType(config.autoIndent, model, range, ch, { - shiftIndent: (indentation) => { - return TypeOperations.shiftIndent(config, indentation); - }, - unshiftIndent: (indentation) => { - return TypeOperations.unshiftIndent(config, indentation); - }, - }, config.languageConfigurationService); - - if (actualIndentation === null) { - return null; - } - - if (actualIndentation !== config.normalizeIndentation(currentIndentation)) { - const firstNonWhitespace = model.getLineFirstNonWhitespaceColumn(range.startLineNumber); - if (firstNonWhitespace === 0) { - return TypeOperations._typeCommand( - new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn), - config.normalizeIndentation(actualIndentation) + ch, - false - ); - } else { - return TypeOperations._typeCommand( - new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn), - config.normalizeIndentation(actualIndentation) + - model.getLineContent(range.startLineNumber).substring(firstNonWhitespace - 1, range.startColumn - 1) + ch, - false - ); - } - } - - return null; - } - - private static _isAutoClosingOvertype(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): boolean { - if (config.autoClosingOvertype === 'never') { - return false; - } - - if (!config.autoClosingPairs.autoClosingPairsCloseSingleChar.has(ch)) { - return false; - } - - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - - if (!selection.isEmpty()) { - return false; - } - - const position = selection.getPosition(); - const lineText = model.getLineContent(position.lineNumber); - const afterCharacter = lineText.charAt(position.column - 1); - - if (afterCharacter !== ch) { - return false; - } - - // Do not over-type quotes after a backslash - const chIsQuote = isQuote(ch); - const beforeCharacter = position.column > 2 ? lineText.charCodeAt(position.column - 2) : CharCode.Null; - if (beforeCharacter === CharCode.Backslash && chIsQuote) { - return false; - } - - // Must over-type a closing character typed by the editor - if (config.autoClosingOvertype === 'auto') { - let found = false; - for (let j = 0, lenJ = autoClosedCharacters.length; j < lenJ; j++) { - const autoClosedCharacter = autoClosedCharacters[j]; - if (position.lineNumber === autoClosedCharacter.startLineNumber && position.column === autoClosedCharacter.startColumn) { - found = true; - break; - } - } - if (!found) { - return false; - } - } - } - - return true; - } - - private static _runAutoClosingOvertype(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - const position = selection.getPosition(); - const typeSelection = new Range(position.lineNumber, position.column, position.lineNumber, position.column + 1); - commands[i] = new ReplaceCommand(typeSelection, ch); - } - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, EditOperationType.TypingOther), - shouldPushStackElementAfter: false - }); - } - - private static _isBeforeClosingBrace(config: CursorConfiguration, lineAfter: string) { - // If the start of lineAfter can be interpretted as both a starting or ending brace, default to returning false - const nextChar = lineAfter.charAt(0); - const potentialStartingBraces = config.autoClosingPairs.autoClosingPairsOpenByStart.get(nextChar) || []; - const potentialClosingBraces = config.autoClosingPairs.autoClosingPairsCloseByStart.get(nextChar) || []; - - const isBeforeStartingBrace = potentialStartingBraces.some(x => lineAfter.startsWith(x.open)); - const isBeforeClosingBrace = potentialClosingBraces.some(x => lineAfter.startsWith(x.close)); - - return !isBeforeStartingBrace && isBeforeClosingBrace; - } - - /** - * Determine if typing `ch` at all `positions` in the `model` results in an - * auto closing open sequence being typed. - * - * Auto closing open sequences can consist of multiple characters, which - * can lead to ambiguities. In such a case, the longest auto-closing open - * sequence is returned. - */ - private static _findAutoClosingPairOpen(config: CursorConfiguration, model: ITextModel, positions: Position[], ch: string): StandardAutoClosingPairConditional | null { - const candidates = config.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch); - if (!candidates) { - return null; - } - - // Determine which auto-closing pair it is - let result: StandardAutoClosingPairConditional | null = null; - for (const candidate of candidates) { - if (result === null || candidate.open.length > result.open.length) { - let candidateIsMatch = true; - for (const position of positions) { - const relevantText = model.getValueInRange(new Range(position.lineNumber, position.column - candidate.open.length + 1, position.lineNumber, position.column)); - if (relevantText + ch !== candidate.open) { - candidateIsMatch = false; - break; - } - } - - if (candidateIsMatch) { - result = candidate; - } - } - } - return result; - } - - /** - * Find another auto-closing pair that is contained by the one passed in. - * - * e.g. when having [(,)] and [(*,*)] as auto-closing pairs - * this method will find [(,)] as a containment pair for [(*,*)] - */ - private static _findContainedAutoClosingPair(config: CursorConfiguration, pair: StandardAutoClosingPairConditional): StandardAutoClosingPairConditional | null { - if (pair.open.length <= 1) { - return null; - } - const lastChar = pair.close.charAt(pair.close.length - 1); - // get candidates with the same last character as close - const candidates = config.autoClosingPairs.autoClosingPairsCloseByEnd.get(lastChar) || []; - let result: StandardAutoClosingPairConditional | null = null; - for (const candidate of candidates) { - if (candidate.open !== pair.open && pair.open.includes(candidate.open) && pair.close.endsWith(candidate.close)) { - if (!result || candidate.open.length > result.open.length) { - result = candidate; - } - } - } - return result; - } - - private static _getAutoClosingPairClose(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean): string | null { - - for (const selection of selections) { - if (!selection.isEmpty()) { - return null; - } - } - - // This method is called both when typing (regularly) and when composition ends - // This means that we need to work with a text buffer where sometimes `ch` is not - // there (it is being typed right now) or with a text buffer where `ch` has already been typed - // - // In order to avoid adding checks for `chIsAlreadyTyped` in all places, we will work - // with two conceptual positions, the position before `ch` and the position after `ch` - // - const positions: { lineNumber: number; beforeColumn: number; afterColumn: number }[] = selections.map((s) => { - const position = s.getPosition(); - if (chIsAlreadyTyped) { - return { lineNumber: position.lineNumber, beforeColumn: position.column - ch.length, afterColumn: position.column }; - } else { - return { lineNumber: position.lineNumber, beforeColumn: position.column, afterColumn: position.column }; - } - }); - - - // Find the longest auto-closing open pair in case of multiple ending in `ch` - // e.g. when having [f","] and [","], it picks [f","] if the character before is f - const pair = this._findAutoClosingPairOpen(config, model, positions.map(p => new Position(p.lineNumber, p.beforeColumn)), ch); - if (!pair) { - return null; - } - - let autoCloseConfig: EditorAutoClosingStrategy; - let shouldAutoCloseBefore: (ch: string) => boolean; - - const chIsQuote = isQuote(ch); - if (chIsQuote) { - autoCloseConfig = config.autoClosingQuotes; - shouldAutoCloseBefore = config.shouldAutoCloseBefore.quote; - } else { - const pairIsForComments = config.blockCommentStartToken ? pair.open.includes(config.blockCommentStartToken) : false; - if (pairIsForComments) { - autoCloseConfig = config.autoClosingComments; - shouldAutoCloseBefore = config.shouldAutoCloseBefore.comment; - } else { - autoCloseConfig = config.autoClosingBrackets; - shouldAutoCloseBefore = config.shouldAutoCloseBefore.bracket; - } - } - - if (autoCloseConfig === 'never') { - return null; - } - - // Sometimes, it is possible to have two auto-closing pairs that have a containment relationship - // e.g. when having [(,)] and [(*,*)] - // - when typing (, the resulting state is (|) - // - when typing *, the desired resulting state is (*|*), not (*|*)) - const containedPair = this._findContainedAutoClosingPair(config, pair); - const containedPairClose = containedPair ? containedPair.close : ''; - let isContainedPairPresent = true; - - for (const position of positions) { - const { lineNumber, beforeColumn, afterColumn } = position; - const lineText = model.getLineContent(lineNumber); - const lineBefore = lineText.substring(0, beforeColumn - 1); - const lineAfter = lineText.substring(afterColumn - 1); - - if (!lineAfter.startsWith(containedPairClose)) { - isContainedPairPresent = false; - } - - // Only consider auto closing the pair if an allowed character follows or if another autoclosed pair closing brace follows - if (lineAfter.length > 0) { - const characterAfter = lineAfter.charAt(0); - const isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, lineAfter); - - if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) { - return null; - } - } - - // Do not auto-close ' or " after a word character - if (pair.open.length === 1 && (ch === '\'' || ch === '"') && autoCloseConfig !== 'always') { - const wordSeparators = getMapForWordSeparators(config.wordSeparators, []); - if (lineBefore.length > 0) { - const characterBefore = lineBefore.charCodeAt(lineBefore.length - 1); - if (wordSeparators.get(characterBefore) === WordCharacterClass.Regular) { - return null; - } - } - } - - if (!model.tokenization.isCheapToTokenize(lineNumber)) { - // Do not force tokenization - return null; - } - - model.tokenization.forceTokenization(lineNumber); - const lineTokens = model.tokenization.getLineTokens(lineNumber); - const scopedLineTokens = createScopedLineTokens(lineTokens, beforeColumn - 1); - if (!pair.shouldAutoClose(scopedLineTokens, beforeColumn - scopedLineTokens.firstCharOffset)) { - return null; - } - - // Typing for example a quote could either start a new string, in which case auto-closing is desirable - // or it could end a previously started string, in which case auto-closing is not desirable - // - // In certain cases, it is really not possible to look at the previous token to determine - // what would happen. That's why we do something really unusual, we pretend to type a different - // character and ask the tokenizer what the outcome of doing that is: after typing a neutral - // character, are we in a string (i.e. the quote would most likely end a string) or not? - // - const neutralCharacter = pair.findNeutralCharacter(); - if (neutralCharacter) { - const tokenType = model.tokenization.getTokenTypeIfInsertingCharacter(lineNumber, beforeColumn, neutralCharacter); - if (!pair.isOK(tokenType)) { - return null; - } - } - } - - if (isContainedPairPresent) { - return pair.close.substring(0, pair.close.length - containedPairClose.length); - } else { - return pair.close; - } - } - - private static _runAutoClosingOpenCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean, autoClosingPairClose: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - commands[i] = new TypeWithAutoClosingCommand(selection, ch, !chIsAlreadyTyped, autoClosingPairClose); - } - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false - }); - } - - private static _shouldSurroundChar(config: CursorConfiguration, ch: string): boolean { - if (isQuote(ch)) { - return (config.autoSurround === 'quotes' || config.autoSurround === 'languageDefined'); - } else { - // Character is a bracket - return (config.autoSurround === 'brackets' || config.autoSurround === 'languageDefined'); - } - } - - private static _isSurroundSelectionType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean { - if (!TypeOperations._shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { - return false; - } - - const isTypingAQuoteCharacter = isQuote(ch); - - for (const selection of selections) { - - if (selection.isEmpty()) { - return false; - } - - let selectionContainsOnlyWhitespace = true; - - for (let lineNumber = selection.startLineNumber; lineNumber <= selection.endLineNumber; lineNumber++) { - const lineText = model.getLineContent(lineNumber); - const startIndex = (lineNumber === selection.startLineNumber ? selection.startColumn - 1 : 0); - const endIndex = (lineNumber === selection.endLineNumber ? selection.endColumn - 1 : lineText.length); - const selectedText = lineText.substring(startIndex, endIndex); - if (/[^ \t]/.test(selectedText)) { - // this selected text contains something other than whitespace - selectionContainsOnlyWhitespace = false; - break; - } - } - - if (selectionContainsOnlyWhitespace) { - return false; - } - - if (isTypingAQuoteCharacter && selection.startLineNumber === selection.endLineNumber && selection.startColumn + 1 === selection.endColumn) { - const selectionText = model.getValueInRange(selection); - if (isQuote(selectionText)) { - // Typing a quote character on top of another quote character - // => disable surround selection type - return false; - } - } - } - - return true; - } - - private static _runSurroundSelectionType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const selection = selections[i]; - const closeCharacter = config.surroundingPairs[ch]; - commands[i] = new SurroundSelectionCommand(selection, ch, closeCharacter); - } - return new EditOperationResult(EditOperationType.Other, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: true - }); - } - - private static _isTypeInterceptorElectricChar(config: CursorConfiguration, model: ITextModel, selections: Selection[]) { - if (selections.length === 1 && model.tokenization.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) { - return true; - } - return false; - } - - private static _typeInterceptorElectricChar(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selection: Selection, ch: string): EditOperationResult | null { - if (!config.electricChars.hasOwnProperty(ch) || !selection.isEmpty()) { - return null; - } - - const position = selection.getPosition(); - model.tokenization.forceTokenization(position.lineNumber); - const lineTokens = model.tokenization.getLineTokens(position.lineNumber); - - let electricAction: IElectricAction | null; - try { - electricAction = config.onElectricCharacter(ch, lineTokens, position.column); - } catch (e) { - onUnexpectedError(e); - return null; - } - - if (!electricAction) { - return null; - } - - if (electricAction.matchOpenBracket) { - const endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1; - const match = model.bracketPairs.findMatchingBracketUp(electricAction.matchOpenBracket, { - lineNumber: position.lineNumber, - column: endColumn - }, 500 /* give at most 500ms to compute */); - - if (match) { - if (match.startLineNumber === position.lineNumber) { - // matched something on the same line => no change in indentation - return null; - } - const matchLine = model.getLineContent(match.startLineNumber); - const matchLineIndentation = strings.getLeadingWhitespace(matchLine); - const newIndentation = config.normalizeIndentation(matchLineIndentation); - - const lineText = model.getLineContent(position.lineNumber); - const lineFirstNonBlankColumn = model.getLineFirstNonWhitespaceColumn(position.lineNumber) || position.column; - - const prefix = lineText.substring(lineFirstNonBlankColumn - 1, position.column - 1); - const typeText = newIndentation + prefix + ch; - - const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, position.column); - - const command = new ReplaceCommand(typeSelection, typeText); - return new EditOperationResult(getTypingOperation(typeText, prevEditOperationType), [command], { - shouldPushStackElementBefore: false, - shouldPushStackElementAfter: true - }); - } - } - - return null; + return CompositionOperation.getEdits(prevEditOperationType, config, model, selections, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta); } /** @@ -871,7 +106,7 @@ export class TypeOperations { if (hasDeletion) { // Check if this could have been a surround selection - if (!TypeOperations._shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { + if (!shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) { return null; } @@ -914,18 +149,14 @@ export class TypeOperations { }); } - if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { - // Unfortunately, the close character is at this point "doubled", so we need to delete it... - const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false)); - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false - }); + const autoClosingOvertypeEdits = AutoClosingOvertypeWithInterceptorsOperation.getEdits(config, model, selections, autoClosedCharacters, ch); + if (autoClosingOvertypeEdits !== undefined) { + return autoClosingOvertypeEdits; } - const autoClosingPairClose = this._getAutoClosingPairClose(config, model, selections, ch, true); - if (autoClosingPairClose !== null) { - return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, true, autoClosingPairClose); + const autoClosingOpenCharEdits = AutoClosingOpenCharTypeOperation.getEdits(config, model, selections, ch, true, false); + if (autoClosingOpenCharEdits !== undefined) { + return autoClosingOpenCharEdits; } return null; @@ -933,149 +164,41 @@ export class TypeOperations { public static typeWithInterceptors(isDoingComposition: boolean, prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult { - if (!isDoingComposition && ch === '\n') { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = TypeOperations._enter(config, model, false, selections[i]); - } - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false, - }); + const enterEdits = EnterOperation.getEdits(config, model, selections, ch, isDoingComposition); + if (enterEdits !== undefined) { + return enterEdits; } - if (!isDoingComposition && this._isAutoIndentType(config, model, selections)) { - const commands: Array = []; - let autoIndentFails = false; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = this._runAutoIndentType(config, model, selections[i], ch); - if (!commands[i]) { - autoIndentFails = true; - break; - } - } - if (!autoIndentFails) { - return new EditOperationResult(EditOperationType.TypingOther, commands, { - shouldPushStackElementBefore: true, - shouldPushStackElementAfter: false, - }); - } + const autoIndentEdits = AutoIndentOperation.getEdits(config, model, selections, ch, isDoingComposition); + if (autoIndentEdits !== undefined) { + return autoIndentEdits; } - if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { - return this._runAutoClosingOvertype(prevEditOperationType, config, model, selections, ch); + const autoClosingOverTypeEdits = AutoClosingOvertypeOperation.getEdits(prevEditOperationType, config, model, selections, autoClosedCharacters, ch); + if (autoClosingOverTypeEdits !== undefined) { + return autoClosingOverTypeEdits; } - if (!isDoingComposition) { - const autoClosingPairClose = this._getAutoClosingPairClose(config, model, selections, ch, false); - if (autoClosingPairClose) { - return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, false, autoClosingPairClose); - } + const autoClosingOpenCharEdits = AutoClosingOpenCharTypeOperation.getEdits(config, model, selections, ch, false, isDoingComposition); + if (autoClosingOpenCharEdits !== undefined) { + return autoClosingOpenCharEdits; } - if (!isDoingComposition && this._isSurroundSelectionType(config, model, selections, ch)) { - return this._runSurroundSelectionType(prevEditOperationType, config, model, selections, ch); + const surroundSelectionEdits = SurroundSelectionOperation.getEdits(config, model, selections, ch, isDoingComposition); + if (surroundSelectionEdits !== undefined) { + return surroundSelectionEdits; } - // Electric characters make sense only when dealing with a single cursor, - // as multiple cursors typing brackets for example would interfer with bracket matching - if (!isDoingComposition && this._isTypeInterceptorElectricChar(config, model, selections)) { - const r = this._typeInterceptorElectricChar(prevEditOperationType, config, model, selections[0], ch); - if (r) { - return r; - } + const interceptorElectricCharOperation = InterceptorElectricCharOperation.getEdits(prevEditOperationType, config, model, selections, ch, isDoingComposition); + if (interceptorElectricCharOperation !== undefined) { + return interceptorElectricCharOperation; } - // A simple character type - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = new ReplaceCommand(selections[i], ch); - } - - const opType = getTypingOperation(ch, prevEditOperationType); - return new EditOperationResult(opType, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), - shouldPushStackElementAfter: false - }); + return SimpleCharacterTypeOperation.getEdits(prevEditOperationType, selections, ch); } public static typeWithoutInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], str: string): EditOperationResult { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = new ReplaceCommand(selections[i], str); - } - const opType = getTypingOperation(str, prevEditOperationType); - return new EditOperationResult(opType, commands, { - shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType), - shouldPushStackElementAfter: false - }); - } - - public static lineInsertBefore(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { - if (model === null || selections === null) { - return []; - } - - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - let lineNumber = selections[i].positionLineNumber; - - if (lineNumber === 1) { - commands[i] = new ReplaceCommandWithoutChangingPosition(new Range(1, 1, 1, 1), '\n'); - } else { - lineNumber--; - const column = model.getLineMaxColumn(lineNumber); - - commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); - } - } - return commands; - } - - public static lineInsertAfter(config: CursorConfiguration, model: ITextModel | null, selections: Selection[] | null): ICommand[] { - if (model === null || selections === null) { - return []; - } - - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - const lineNumber = selections[i].positionLineNumber; - const column = model.getLineMaxColumn(lineNumber); - commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column)); - } - return commands; - } - - public static lineBreakInsert(config: CursorConfiguration, model: ITextModel, selections: Selection[]): ICommand[] { - const commands: ICommand[] = []; - for (let i = 0, len = selections.length; i < len; i++) { - commands[i] = this._enter(config, model, true, selections[i]); - } - return commands; - } -} - -export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState { - - private readonly _openCharacter: string; - private readonly _closeCharacter: string; - public closeCharacterRange: Range | null; - public enclosingRange: Range | null; - - constructor(selection: Selection, openCharacter: string, insertOpenCharacter: boolean, closeCharacter: string) { - super(selection, (insertOpenCharacter ? openCharacter : '') + closeCharacter, 0, -closeCharacter.length); - this._openCharacter = openCharacter; - this._closeCharacter = closeCharacter; - this.closeCharacterRange = null; - this.enclosingRange = null; - } - - public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { - const inverseEditOperations = helper.getInverseEditOperations(); - const range = inverseEditOperations[0].range; - this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn); - this.enclosingRange = new Range(range.startLineNumber, range.endColumn - this._openCharacter.length - this._closeCharacter.length, range.endLineNumber, range.endColumn); - return super.computeCursorState(model, helper); + return TypeWithoutInterceptorsOperation.getEdits(prevEditOperationType, selections, str); } } @@ -1089,40 +212,3 @@ export class CompositionOutcome { public readonly insertedSelectionEnd: number, ) { } } - -function getTypingOperation(typedText: string, previousTypingOperation: EditOperationType): EditOperationType { - if (typedText === ' ') { - return previousTypingOperation === EditOperationType.TypingFirstSpace - || previousTypingOperation === EditOperationType.TypingConsecutiveSpace - ? EditOperationType.TypingConsecutiveSpace - : EditOperationType.TypingFirstSpace; - } - - return EditOperationType.TypingOther; -} - -function shouldPushStackElementBetween(previousTypingOperation: EditOperationType, typingOperation: EditOperationType): boolean { - if (isTypingOperation(previousTypingOperation) && !isTypingOperation(typingOperation)) { - // Always set an undo stop before non-type operations - return true; - } - if (previousTypingOperation === EditOperationType.TypingFirstSpace) { - // `abc |d`: No undo stop - // `abc |d`: Undo stop - return false; - } - // Insert undo stop between different operation types - return normalizeOperationType(previousTypingOperation) !== normalizeOperationType(typingOperation); -} - -function normalizeOperationType(type: EditOperationType): EditOperationType | 'space' { - return (type === EditOperationType.TypingConsecutiveSpace || type === EditOperationType.TypingFirstSpace) - ? 'space' - : type; -} - -function isTypingOperation(type: EditOperationType): boolean { - return type === EditOperationType.TypingOther - || type === EditOperationType.TypingFirstSpace - || type === EditOperationType.TypingConsecutiveSpace; -} diff --git a/src/vs/editor/common/cursor/cursorWordOperations.ts b/src/vs/editor/common/cursor/cursorWordOperations.ts index b16172cc89a..a43538215b2 100644 --- a/src/vs/editor/common/cursor/cursorWordOperations.ts +++ b/src/vs/editor/common/cursor/cursorWordOperations.ts @@ -208,7 +208,7 @@ export class WordOperations { return 0; } - public static moveWordLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position { + public static moveWordLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { let lineNumber = position.lineNumber; let column = position.column; @@ -227,7 +227,8 @@ export class WordOperations { if (wordNavigationType === WordNavigationType.WordStartFast) { if ( - prevWordOnLine + !hasMulticursor // avoid having multiple cursors stop at different locations when doing word start + && prevWordOnLine && prevWordOnLine.wordType === WordType.Separator && prevWordOnLine.end - prevWordOnLine.start === 1 && prevWordOnLine.nextCharClass === WordCharacterClass.Regular @@ -830,10 +831,10 @@ export class WordPartOperations extends WordOperations { return candidates[0]; } - public static moveWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position): Position { + public static moveWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, hasMulticursor: boolean): Position { const candidates = enforceDefined([ - WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordStart), - WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordEnd), + WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordStart, hasMulticursor), + WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordEnd, hasMulticursor), WordOperations._moveWordPartLeft(model, position) ]); candidates.sort(Position.compare); diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts index 50fbf66deba..2de4635030d 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/algorithms/diffAlgorithm.ts @@ -52,6 +52,18 @@ export class SequenceDiff { ); } + public static assertSorted(sequenceDiffs: SequenceDiff[]): void { + let last: SequenceDiff | undefined = undefined; + for (const cur of sequenceDiffs) { + if (last) { + if (!(last.seq1Range.endExclusive <= cur.seq1Range.start && last.seq2Range.endExclusive <= cur.seq2Range.start)) { + throw new BugIndicatingError('Sequence diffs must be sorted'); + } + } + last = cur; + } + } + constructor( public readonly seq1Range: OffsetRange, public readonly seq2Range: OffsetRange, diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts index 8f7183211d7..6a46557a177 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/computeMovedLines.ts @@ -9,10 +9,10 @@ import { pushMany, compareBy, numberComparator, reverseOrder } from 'vs/base/com import { MonotonousArray, findLastMonotonous } from 'vs/base/common/arraysFind'; import { SetMap } from 'vs/base/common/map'; import { LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange'; -import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence'; import { LineRangeFragment, isSpace } from 'vs/editor/common/diff/defaultLinesDiffComputer/utils'; import { MyersDiffAlgorithm } from 'vs/editor/common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm'; +import { Range } from 'vs/editor/common/core/range'; export function computeMovedLines( changes: DetailedLineRangeMapping[], @@ -260,8 +260,8 @@ function areLinesSimilar(line1: string, line2: string, timeout: ITimeout): boole const myersDiffingAlgorithm = new MyersDiffAlgorithm(); const result = myersDiffingAlgorithm.compute( - new LinesSliceCharSequence([line1], new OffsetRange(0, 1), false), - new LinesSliceCharSequence([line2], new OffsetRange(0, 1), false), + new LinesSliceCharSequence([line1], new Range(1, 1, 1, line1.length), false), + new LinesSliceCharSequence([line2], new Range(1, 1, 1, line2.length), false), timeout ); let commonNonSpaceCharCount = 0; diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts index a25f55c1ae5..5573f684526 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts @@ -17,7 +17,7 @@ import { extendDiffsToEntireWordIfAppropriate, optimizeSequenceDiffs, removeShor import { LineSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/lineSequence'; import { LinesSliceCharSequence } from 'vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence'; import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from 'vs/editor/common/diff/linesDiffComputer'; -import { DetailedLineRangeMapping, RangeMapping } from '../rangeMapping'; +import { DetailedLineRangeMapping, LineRangeMapping, RangeMapping } from '../rangeMapping'; export class DefaultLinesDiffComputer implements ILinesDiffComputer { private readonly dynamicProgrammingDiffing = new DynamicProgrammingDiffing(); @@ -80,7 +80,8 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { return this.myersDiffingAlgorithm.compute( sequence1, - sequence2 + sequence2, + timeout ); })(); @@ -166,7 +167,9 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { for (const ic of c.innerChanges) { const valid = validatePosition(ic.modifiedRange.getStartPosition(), modifiedLines) && validatePosition(ic.modifiedRange.getEndPosition(), modifiedLines) && validatePosition(ic.originalRange.getStartPosition(), originalLines) && validatePosition(ic.originalRange.getEndPosition(), originalLines); - if (!valid) { return false; } + if (!valid) { + return false; + } } if (!validateRange(c.modified, modifiedLines) || !validateRange(c.original, originalLines)) { return false; @@ -207,18 +210,28 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { } private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } { - const slice1 = new LinesSliceCharSequence(originalLines, diff.seq1Range, considerWhitespaceChanges); - const slice2 = new LinesSliceCharSequence(modifiedLines, diff.seq2Range, considerWhitespaceChanges); + const lineRangeMapping = toLineRangeMapping(diff); + const rangeMapping = lineRangeMapping.toRangeMapping2(originalLines, modifiedLines); + + const slice1 = new LinesSliceCharSequence(originalLines, rangeMapping.originalRange, considerWhitespaceChanges); + const slice2 = new LinesSliceCharSequence(modifiedLines, rangeMapping.modifiedRange, considerWhitespaceChanges); const diffResult = slice1.length + slice2.length < 500 ? this.dynamicProgrammingDiffing.compute(slice1, slice2, timeout) : this.myersDiffingAlgorithm.compute(slice1, slice2, timeout); + const check = false; + let diffs = diffResult.diffs; + if (check) { SequenceDiff.assertSorted(diffs); } diffs = optimizeSequenceDiffs(slice1, slice2, diffs); + if (check) { SequenceDiff.assertSorted(diffs); } diffs = extendDiffsToEntireWordIfAppropriate(slice1, slice2, diffs); + if (check) { SequenceDiff.assertSorted(diffs); } diffs = removeShortMatches(slice1, slice2, diffs); + if (check) { SequenceDiff.assertSorted(diffs); } diffs = removeVeryShortMatchingTextBetweenLongDiffs(slice1, slice2, diffs); + if (check) { SequenceDiff.assertSorted(diffs); } const result = diffs.map( (d) => @@ -228,6 +241,8 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { ) ); + if (check) { RangeMapping.assertSorted(result); } + // Assert: result applied on original should be the same as diff applied to original return { @@ -311,3 +326,10 @@ export function getLineRangeMapping(rangeMapping: RangeMapping, originalLines: s return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, [rangeMapping]); } + +function toLineRangeMapping(sequenceDiff: SequenceDiff) { + return new LineRangeMapping( + new LineRange(sequenceDiff.seq1Range.start + 1, sequenceDiff.seq1Range.endExclusive + 1), + new LineRange(sequenceDiff.seq2Range.start + 1, sequenceDiff.seq2Range.endExclusive + 1), + ); +} diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts index 9edc63d335f..7761599484b 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/linesSliceCharSequence.ts @@ -13,52 +13,39 @@ import { isSpace } from 'vs/editor/common/diff/defaultLinesDiffComputer/utils'; export class LinesSliceCharSequence implements ISequence { private readonly elements: number[] = []; - private readonly firstCharOffsetByLine: number[] = []; - public readonly lineRange: OffsetRange; - // To account for trimming - private readonly additionalOffsetByLine: number[] = []; + private readonly firstElementOffsetByLineIdx: number[] = []; + private readonly lineStartOffsets: number[] = []; + private readonly trimmedWsLengthsByLineIdx: number[] = []; - constructor(public readonly lines: string[], lineRange: OffsetRange, public readonly considerWhitespaceChanges: boolean) { - // This slice has to have lineRange.length many \n! (otherwise diffing against an empty slice will be problematic) - // (Unless it covers the entire document, in that case the other slice also has to cover the entire document ands it's okay) + constructor(public readonly lines: string[], private readonly range: Range, public readonly considerWhitespaceChanges: boolean) { + this.firstElementOffsetByLineIdx.push(0); + for (let lineNumber = this.range.startLineNumber; lineNumber <= this.range.endLineNumber; lineNumber++) { + let line = lines[lineNumber - 1]; + let lineStartOffset = 0; + if (lineNumber === this.range.startLineNumber && this.range.startColumn > 1) { + lineStartOffset = this.range.startColumn - 1; + line = line.substring(lineStartOffset); + } + this.lineStartOffsets.push(lineStartOffset); - // If the slice covers the end, but does not start at the beginning, we include just the \n of the previous line. - let trimFirstLineFully = false; - if (lineRange.start > 0 && lineRange.endExclusive >= lines.length) { - lineRange = new OffsetRange(lineRange.start - 1, lineRange.endExclusive); - trimFirstLineFully = true; - } - - this.lineRange = lineRange; - - this.firstCharOffsetByLine[0] = 0; - for (let i = this.lineRange.start; i < this.lineRange.endExclusive; i++) { - let line = lines[i]; - let offset = 0; - if (trimFirstLineFully) { - offset = line.length; - line = ''; - trimFirstLineFully = false; - } else if (!considerWhitespaceChanges) { + let trimmedWsLength = 0; + if (!considerWhitespaceChanges) { const trimmedStartLine = line.trimStart(); - offset = line.length - trimmedStartLine.length; + trimmedWsLength = line.length - trimmedStartLine.length; line = trimmedStartLine.trimEnd(); } + this.trimmedWsLengthsByLineIdx.push(trimmedWsLength); - this.additionalOffsetByLine.push(offset); - - for (let i = 0; i < line.length; i++) { + const lineLength = lineNumber === this.range.endLineNumber ? Math.min(this.range.endColumn - 1 - lineStartOffset - trimmedWsLength, line.length) : line.length; + for (let i = 0; i < lineLength; i++) { this.elements.push(line.charCodeAt(i)); } - // Don't add an \n that does not exist in the document. - if (i < lines.length - 1) { + if (lineNumber < this.range.endLineNumber) { this.elements.push('\n'.charCodeAt(0)); - this.firstCharOffsetByLine[i - this.lineRange.start + 1] = this.elements.length; + this.firstElementOffsetByLineIdx.push(this.elements.length); } } - // To account for the last line - this.additionalOffsetByLine.push(0); } toString() { @@ -111,18 +98,23 @@ export class LinesSliceCharSequence implements ISequence { return score; } - public translateOffset(offset: number): Position { + public translateOffset(offset: number, preference: 'left' | 'right' = 'right'): Position { // find smallest i, so that lineBreakOffsets[i] <= offset using binary search - if (this.lineRange.isEmpty) { - return new Position(this.lineRange.start + 1, 1); - } - - const i = findLastIdxMonotonous(this.firstCharOffsetByLine, (value) => value <= offset); - return new Position(this.lineRange.start + i + 1, offset - this.firstCharOffsetByLine[i] + this.additionalOffsetByLine[i] + 1); + const i = findLastIdxMonotonous(this.firstElementOffsetByLineIdx, (value) => value <= offset); + const lineOffset = offset - this.firstElementOffsetByLineIdx[i]; + return new Position( + this.range.startLineNumber + i, + 1 + this.lineStartOffsets[i] + lineOffset + ((lineOffset === 0 && preference === 'left') ? 0 : this.trimmedWsLengthsByLineIdx[i]) + ); } public translateRange(range: OffsetRange): Range { - return Range.fromPositions(this.translateOffset(range.start), this.translateOffset(range.endExclusive)); + const pos1 = this.translateOffset(range.start, 'right'); + const pos2 = this.translateOffset(range.endExclusive, 'left'); + if (pos2.isBefore(pos1)) { + return Range.fromPositions(pos2, pos2); + } + return Range.fromPositions(pos1, pos2); } /** @@ -161,8 +153,8 @@ export class LinesSliceCharSequence implements ISequence { } public extendToFullLines(range: OffsetRange): OffsetRange { - const start = findLastMonotonous(this.firstCharOffsetByLine, x => x <= range.start) ?? 0; - const end = findFirstMonotonous(this.firstCharOffsetByLine, x => range.endExclusive <= x) ?? this.elements.length; + const start = findLastMonotonous(this.firstElementOffsetByLineIdx, x => x <= range.start) ?? 0; + const end = findFirstMonotonous(this.firstElementOffsetByLineIdx, x => range.endExclusive <= x) ?? this.elements.length; return new OffsetRange(start, end); } } diff --git a/src/vs/editor/common/diff/rangeMapping.ts b/src/vs/editor/common/diff/rangeMapping.ts index f15b2a05310..da9c3a49109 100644 --- a/src/vs/editor/common/diff/rangeMapping.ts +++ b/src/vs/editor/common/diff/rangeMapping.ts @@ -5,6 +5,7 @@ import { BugIndicatingError } from 'vs/base/common/errors'; import { LineRange } from 'vs/editor/common/core/lineRange'; +import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { AbstractText, SingleTextEdit } from 'vs/editor/common/core/textEdit'; @@ -118,6 +119,70 @@ export class LineRangeMapping { ); } } + + /** + * This method assumes that the LineRangeMapping describes a valid diff! + * I.e. if one range is empty, the other range cannot be the entire document. + * It avoids various problems when the line range points to non-existing line-numbers. + */ + public toRangeMapping2(original: string[], modified: string[]): RangeMapping { + if (isValidLineNumber(this.original.endLineNumberExclusive, original) + && isValidLineNumber(this.modified.endLineNumberExclusive, modified)) { + return new RangeMapping( + new Range(this.original.startLineNumber, 1, this.original.endLineNumberExclusive, 1), + new Range(this.modified.startLineNumber, 1, this.modified.endLineNumberExclusive, 1), + ); + } + + if (!this.original.isEmpty && !this.modified.isEmpty) { + return new RangeMapping( + Range.fromPositions( + new Position(this.original.startLineNumber, 1), + normalizePosition(new Position(this.original.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), original) + ), + Range.fromPositions( + new Position(this.modified.startLineNumber, 1), + normalizePosition(new Position(this.modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), modified) + ), + ); + } + + if (this.original.startLineNumber > 1 && this.modified.startLineNumber > 1) { + return new RangeMapping( + Range.fromPositions( + normalizePosition(new Position(this.original.startLineNumber - 1, Number.MAX_SAFE_INTEGER), original), + normalizePosition(new Position(this.original.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), original) + ), + Range.fromPositions( + normalizePosition(new Position(this.modified.startLineNumber - 1, Number.MAX_SAFE_INTEGER), modified), + normalizePosition(new Position(this.modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), modified) + ), + ); + } + + // Situation now: one range is empty and one range touches the last line and one range starts at line 1. + // I don't think this can happen. + + throw new BugIndicatingError(); + } +} + +function normalizePosition(position: Position, content: string[]): Position { + if (position.lineNumber < 1) { + return new Position(1, 1); + } + if (position.lineNumber > content.length) { + return new Position(content.length, content[content.length - 1].length + 1); + } + const line = content[position.lineNumber - 1]; + if (position.column > line.length + 1) { + return new Position(position.lineNumber, line.length + 1); + } + return position; +} + +function isValidLineNumber(lineNumber: number, lines: string[]): boolean { + return lineNumber >= 1 && lineNumber <= lines.length; } /** @@ -161,6 +226,19 @@ export class DetailedLineRangeMapping extends LineRangeMapping { * Maps a range in the original text model to a range in the modified text model. */ export class RangeMapping { + public static assertSorted(rangeMappings: RangeMapping[]): void { + for (let i = 1; i < rangeMappings.length; i++) { + const previous = rangeMappings[i - 1]; + const current = rangeMappings[i]; + if (!( + previous.originalRange.getEndPosition().isBeforeOrEqual(current.originalRange.getStartPosition()) + && previous.modifiedRange.getEndPosition().isBeforeOrEqual(current.modifiedRange.getStartPosition()) + )) { + throw new BugIndicatingError('Range mappings must be sorted'); + } + } + } + /** * The original range. */ diff --git a/src/vs/editor/common/languageFeatureRegistry.ts b/src/vs/editor/common/languageFeatureRegistry.ts index 53c14ac57b9..47679f308f4 100644 --- a/src/vs/editor/common/languageFeatureRegistry.ts +++ b/src/vs/editor/common/languageFeatureRegistry.ts @@ -109,6 +109,10 @@ export class LanguageFeatureRegistry { return result; } + allNoModel(): T[] { + return this._entries.map(entry => entry.provider); + } + ordered(model: ITextModel): T[] { const result: T[] = []; this._orderedForEach(model, entry => result.push(entry.provider)); diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 68fbf632fb4..3159cdc6ae5 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -687,6 +687,11 @@ export interface InlineCompletionContext { */ readonly triggerKind: InlineCompletionTriggerKind; readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined; + /** + * @experimental + * @internal + */ + readonly userPrompt?: string | undefined; } export class SelectedSuggestionInfo { @@ -765,6 +770,12 @@ export type InlineCompletionProviderGroupId = string; export interface InlineCompletionsProvider { provideInlineCompletions(model: model.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + /** + * @experimental + * @internal + */ + provideInlineEdits?(model: model.ITextModel, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + /** * Will be called when an item is shown. * @param updatedInsertText Is useful to understand bracket completion. @@ -1847,6 +1858,11 @@ export interface CommentInput { uri: URI; } +export interface CommentThreadRevealOptions { + preserveFocus: boolean; + focusReply: boolean; +} + /** * @internal */ @@ -2209,6 +2225,14 @@ export interface DocumentDropEdit { additionalEdit?: WorkspaceEdit; } +/** + * @internal + */ +export interface DocumentDropEditsSession { + edits: readonly DocumentDropEdit[]; + dispose(): void; +} + /** * @internal */ @@ -2216,7 +2240,7 @@ export interface DocumentDropEditProvider { readonly id?: string; readonly dropMimeTypes?: readonly string[]; - provideDocumentDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; + provideDocumentDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult; resolveDocumentDropEdit?(edit: DocumentDropEdit, token: CancellationToken): Promise; } @@ -2238,7 +2262,7 @@ export interface MappedEditsProvider { * * @param document The document to provide mapped edits for. * @param codeBlocks Code blocks that come from an LLM's reply. - * "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. + * "Apply in Editor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. * @param context The context for providing mapped edits. * @param token A cancellation token. * @returns A provider result of text edits. diff --git a/src/vs/editor/common/languages/autoIndent.ts b/src/vs/editor/common/languages/autoIndent.ts index 9ae3df974aa..5c643b4fa60 100644 --- a/src/vs/editor/common/languages/autoIndent.ts +++ b/src/vs/editor/common/languages/autoIndent.ts @@ -7,17 +7,19 @@ import * as strings from 'vs/base/common/strings'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { IndentAction } from 'vs/editor/common/languages/languageConfiguration'; -import { createScopedLineTokens } from 'vs/editor/common/languages/supports'; -import { IndentConsts, IndentRulesSupport } from 'vs/editor/common/languages/supports/indentRules'; +import { IndentConsts } from 'vs/editor/common/languages/supports/indentRules'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; -import { getScopedLineTokens, ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { IndentationContextProcessor, isLanguageDifferentFromLineStart, ProcessedIndentRulesSupport } from 'vs/editor/common/languages/supports/indentationLineProcessor'; +import { CursorConfiguration } from 'vs/editor/common/cursorCommon'; export interface IVirtualModel { tokenization: { - getLineTokens(lineNumber: number): LineTokens; + getLineTokens(lineNumber: number): IViewLineTokens; getLanguageId(): string; getLanguageIdAtPosition(lineNumber: number, column: number): string; + forceTokenization?(lineNumber: number): void; }; getLineContent(lineNumber: number): string; } @@ -35,7 +37,7 @@ export interface IIndentConverter { * 0: every line above are invalid * else: nearest preceding line of the same language */ -function getPrecedingValidLine(model: IVirtualModel, lineNumber: number, indentRulesSupport: IndentRulesSupport) { +function getPrecedingValidLine(model: IVirtualModel, lineNumber: number, processedIndentRulesSupport: ProcessedIndentRulesSupport) { const languageId = model.tokenization.getLanguageIdAtPosition(lineNumber, 0); if (lineNumber > 1) { let lastLineNumber: number; @@ -46,7 +48,7 @@ function getPrecedingValidLine(model: IVirtualModel, lineNumber: number, indentR return resultLineNumber; } const text = model.getLineContent(lastLineNumber); - if (indentRulesSupport.shouldIgnore(text) || /^\s+$/.test(text) || text === '') { + if (processedIndentRulesSupport.shouldIgnore(lastLineNumber) || /^\s+$/.test(text) || text === '') { resultLineNumber = lastLineNumber; continue; } @@ -85,6 +87,7 @@ export function getInheritIndentForLine( if (!indentRulesSupport) { return null; } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentRulesSupport, languageConfigurationService); if (lineNumber <= 1) { return { @@ -106,7 +109,7 @@ export function getInheritIndentForLine( } } - const precedingUnIgnoredLine = getPrecedingValidLine(model, lineNumber, indentRulesSupport); + const precedingUnIgnoredLine = getPrecedingValidLine(model, lineNumber, processedIndentRulesSupport); if (precedingUnIgnoredLine < 0) { return null; } else if (precedingUnIgnoredLine < 1) { @@ -116,14 +119,15 @@ export function getInheritIndentForLine( }; } - const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine); - if (indentRulesSupport.shouldIncrease(precedingUnIgnoredLineContent) || indentRulesSupport.shouldIndentNextLine(precedingUnIgnoredLineContent)) { + if (processedIndentRulesSupport.shouldIncrease(precedingUnIgnoredLine) || processedIndentRulesSupport.shouldIndentNextLine(precedingUnIgnoredLine)) { + const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine); return { indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), action: IndentAction.Indent, line: precedingUnIgnoredLine }; - } else if (indentRulesSupport.shouldDecrease(precedingUnIgnoredLineContent)) { + } else if (processedIndentRulesSupport.shouldDecrease(precedingUnIgnoredLine)) { + const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine); return { indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), action: null, @@ -150,7 +154,7 @@ export function getInheritIndentForLine( (previousLineIndentMetadata & IndentConsts.INDENT_NEXTLINE_MASK)) { let stopLine = 0; for (let i = previousLine - 1; i > 0; i--) { - if (indentRulesSupport.shouldIndentNextLine(model.getLineContent(i))) { + if (processedIndentRulesSupport.shouldIndentNextLine(i)) { continue; } stopLine = i; @@ -173,17 +177,16 @@ export function getInheritIndentForLine( } else { // search from precedingUnIgnoredLine until we find one whose indent is not temporary for (let i = precedingUnIgnoredLine; i > 0; i--) { - const lineContent = model.getLineContent(i); - if (indentRulesSupport.shouldIncrease(lineContent)) { + if (processedIndentRulesSupport.shouldIncrease(i)) { return { - indentation: strings.getLeadingWhitespace(lineContent), + indentation: strings.getLeadingWhitespace(model.getLineContent(i)), action: IndentAction.Indent, line: i }; - } else if (indentRulesSupport.shouldIndentNextLine(lineContent)) { + } else if (processedIndentRulesSupport.shouldIndentNextLine(i)) { let stopLine = 0; for (let j = i - 1; j > 0; j--) { - if (indentRulesSupport.shouldIndentNextLine(model.getLineContent(i))) { + if (processedIndentRulesSupport.shouldIndentNextLine(i)) { continue; } stopLine = j; @@ -195,9 +198,9 @@ export function getInheritIndentForLine( action: null, line: stopLine + 1 }; - } else if (indentRulesSupport.shouldDecrease(lineContent)) { + } else if (processedIndentRulesSupport.shouldDecrease(i)) { return { - indentation: strings.getLeadingWhitespace(lineContent), + indentation: strings.getLeadingWhitespace(model.getLineContent(i)), action: null, line: i }; @@ -235,8 +238,8 @@ export function getGoodIndentForLine( return null; } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(virtualModel, indentRulesSupport, languageConfigurationService); const indent = getInheritIndentForLine(autoIndent, virtualModel, lineNumber, undefined, languageConfigurationService); - const lineContent = virtualModel.getLineContent(lineNumber); if (indent) { const inheritLine = indent.line; @@ -268,7 +271,7 @@ export function getGoodIndentForLine( indentation = indentConverter.unshiftIndent(indentation); } - if (indentRulesSupport.shouldDecrease(lineContent)) { + if (processedIndentRulesSupport.shouldDecrease(lineNumber)) { indentation = indentConverter.unshiftIndent(indentation); } @@ -281,7 +284,7 @@ export function getGoodIndentForLine( } } - if (indentRulesSupport.shouldDecrease(lineContent)) { + if (processedIndentRulesSupport.shouldDecrease(lineNumber)) { if (indent.action === IndentAction.Indent) { return indent.indentation; } else { @@ -308,80 +311,44 @@ export function getIndentForEnter( if (autoIndent < EditorAutoIndentStrategy.Full) { return null; } - model.tokenization.forceTokenization(range.startLineNumber); - const lineTokens = model.tokenization.getLineTokens(range.startLineNumber); - const scopedLineTokens = createScopedLineTokens(lineTokens, range.startColumn - 1); - const scopedLineText = scopedLineTokens.getLineContent(); - - let embeddedLanguage = false; - let beforeEnterText: string; - if (scopedLineTokens.firstCharOffset > 0 && lineTokens.getLanguageId(0) !== scopedLineTokens.languageId) { - // we are in the embeded language content - embeddedLanguage = true; // if embeddedLanguage is true, then we don't touch the indentation of current line - beforeEnterText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - beforeEnterText = lineTokens.getLineContent().substring(0, range.startColumn - 1); - } - - let afterEnterText: string; - if (range.isEmpty()) { - afterEnterText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - const endScopedLineTokens = getScopedLineTokens(model, range.endLineNumber, range.endColumn); - afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); - } - - const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId).indentRulesSupport; + const languageId = model.getLanguageIdAtPosition(range.startLineNumber, range.startColumn); + const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; if (!indentRulesSupport) { return null; } - const beforeEnterResult = beforeEnterText; - const beforeEnterIndent = strings.getLeadingWhitespace(beforeEnterText); + model.tokenization.forceTokenization(range.startLineNumber); + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContextTokens = indentationContextProcessor.getProcessedTokenContextAroundRange(range); + const afterEnterProcessedTokens = processedContextTokens.afterRangeProcessedTokens; + const beforeEnterProcessedTokens = processedContextTokens.beforeRangeProcessedTokens; + const beforeEnterIndent = strings.getLeadingWhitespace(beforeEnterProcessedTokens.getLineContent()); - const virtualModel: IVirtualModel = { - tokenization: { - getLineTokens: (lineNumber: number) => { - return model.tokenization.getLineTokens(lineNumber); - }, - getLanguageId: () => { - return model.getLanguageId(); - }, - getLanguageIdAtPosition: (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); - }, - }, - getLineContent: (lineNumber: number) => { - if (lineNumber === range.startLineNumber) { - return beforeEnterResult; - } else { - return model.getLineContent(lineNumber); - } - } - }; - - const currentLineIndent = strings.getLeadingWhitespace(lineTokens.getLineContent()); + const virtualModel = createVirtualModelWithModifiedTokensAtLine(model, range.startLineNumber, beforeEnterProcessedTokens); + const languageIsDifferentFromLineStart = isLanguageDifferentFromLineStart(model, range.getStartPosition()); + const currentLine = model.getLineContent(range.startLineNumber); + const currentLineIndent = strings.getLeadingWhitespace(currentLine); const afterEnterAction = getInheritIndentForLine(autoIndent, virtualModel, range.startLineNumber + 1, undefined, languageConfigurationService); if (!afterEnterAction) { - const beforeEnter = embeddedLanguage ? currentLineIndent : beforeEnterIndent; + const beforeEnter = languageIsDifferentFromLineStart ? currentLineIndent : beforeEnterIndent; return { beforeEnter: beforeEnter, afterEnter: beforeEnter }; } - let afterEnterIndent = embeddedLanguage ? currentLineIndent : afterEnterAction.indentation; + let afterEnterIndent = languageIsDifferentFromLineStart ? currentLineIndent : afterEnterAction.indentation; if (afterEnterAction.action === IndentAction.Indent) { afterEnterIndent = indentConverter.shiftIndent(afterEnterIndent); } - if (indentRulesSupport.shouldDecrease(afterEnterText)) { + if (indentRulesSupport.shouldDecrease(afterEnterProcessedTokens.getLineContent())) { afterEnterIndent = indentConverter.unshiftIndent(afterEnterIndent); } return { - beforeEnter: embeddedLanguage ? currentLineIndent : beforeEnterIndent, + beforeEnter: languageIsDifferentFromLineStart ? currentLineIndent : beforeEnterIndent, afterEnter: afterEnterIndent }; } @@ -391,43 +358,39 @@ export function getIndentForEnter( * this line doesn't match decreaseIndentPattern, we should not adjust the indentation. */ export function getIndentActionForType( - autoIndent: EditorAutoIndentStrategy, + cursorConfig: CursorConfiguration, model: ITextModel, range: Range, ch: string, indentConverter: IIndentConverter, languageConfigurationService: ILanguageConfigurationService ): string | null { + const autoIndent = cursorConfig.autoIndent; if (autoIndent < EditorAutoIndentStrategy.Full) { return null; } - const scopedLineTokens = getScopedLineTokens(model, range.startLineNumber, range.startColumn); - - if (scopedLineTokens.firstCharOffset) { + const languageIsDifferentFromLineStart = isLanguageDifferentFromLineStart(model, range.getStartPosition()); + if (languageIsDifferentFromLineStart) { // this line has mixed languages and indentation rules will not work return null; } - const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId).indentRulesSupport; + const languageId = model.getLanguageIdAtPosition(range.startLineNumber, range.startColumn); + const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; if (!indentRulesSupport) { return null; } - const scopedLineText = scopedLineTokens.getLineContent(); - const beforeTypeText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); - - // selection support - let afterTypeText: string; - if (range.isEmpty()) { - afterTypeText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - const endScopedLineTokens = getScopedLineTokens(model, range.endLineNumber, range.endColumn); - afterTypeText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); - } + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContextTokens = indentationContextProcessor.getProcessedTokenContextAroundRange(range); + const beforeRangeText = processedContextTokens.beforeRangeProcessedTokens.getLineContent(); + const afterRangeText = processedContextTokens.afterRangeProcessedTokens.getLineContent(); + const textAroundRange = beforeRangeText + afterRangeText; + const textAroundRangeWithCharacter = beforeRangeText + ch + afterRangeText; // If previous content already matches decreaseIndentPattern, it means indentation of this line should already be adjusted // Users might change the indentation by purpose and we should honor that instead of readjusting. - if (!indentRulesSupport.shouldDecrease(beforeTypeText + afterTypeText) && indentRulesSupport.shouldDecrease(beforeTypeText + ch + afterTypeText)) { + if (!indentRulesSupport.shouldDecrease(textAroundRange) && indentRulesSupport.shouldDecrease(textAroundRangeWithCharacter)) { // after typing `ch`, the content matches decreaseIndentPattern, we should adjust the indent to a good manner. // 1. Get inherited indent action const r = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService); @@ -443,6 +406,29 @@ export function getIndentActionForType( return indentation; } + const previousLineNumber = range.startLineNumber - 1; + if (previousLineNumber > 0) { + const previousLine = model.getLineContent(previousLineNumber); + if (indentRulesSupport.shouldIndentNextLine(previousLine) && indentRulesSupport.shouldIncrease(textAroundRangeWithCharacter)) { + const inheritedIndentationData = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService); + const inheritedIndentation = inheritedIndentationData?.indentation; + if (inheritedIndentation !== undefined) { + const currentLine = model.getLineContent(range.startLineNumber); + const actualCurrentIndentation = strings.getLeadingWhitespace(currentLine); + const inferredCurrentIndentation = indentConverter.shiftIndent(inheritedIndentation); + // If the inferred current indentation is not equal to the actual current indentation, then the indentation has been intentionally changed, in that case keep it + const inferredIndentationEqualsActual = inferredCurrentIndentation === actualCurrentIndentation; + const textAroundRangeContainsOnlyWhitespace = /^\s*$/.test(textAroundRange); + const autoClosingPairs = cursorConfig.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch); + const autoClosingPairExists = autoClosingPairs && autoClosingPairs.length > 0; + const isChFirstNonWhitespaceCharacterAndInAutoClosingPair = autoClosingPairExists && textAroundRangeContainsOnlyWhitespace; + if (inferredIndentationEqualsActual && isChFirstNonWhitespaceCharacterAndInAutoClosingPair) { + return inheritedIndentation; + } + } + } + } + return null; } @@ -460,3 +446,32 @@ export function getIndentMetadata( } return indentRulesSupport.getIndentMetadata(model.getLineContent(lineNumber)); } + +function createVirtualModelWithModifiedTokensAtLine(model: ITextModel, modifiedLineNumber: number, modifiedTokens: IViewLineTokens): IVirtualModel { + const virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number): IViewLineTokens => { + if (lineNumber === modifiedLineNumber) { + return modifiedTokens; + } else { + return model.tokenization.getLineTokens(lineNumber); + } + }, + getLanguageId: (): string => { + return model.getLanguageId(); + }, + getLanguageIdAtPosition: (lineNumber: number, column: number): string => { + return model.getLanguageIdAtPosition(lineNumber, column); + }, + }, + getLineContent: (lineNumber: number): string => { + if (lineNumber === modifiedLineNumber) { + return modifiedTokens.getLineContent(); + } else { + return model.getLineContent(lineNumber); + } + } + }; + return virtualModel; +} + diff --git a/src/vs/editor/common/languages/enterAction.ts b/src/vs/editor/common/languages/enterAction.ts index 447665fe816..27669db6ebe 100644 --- a/src/vs/editor/common/languages/enterAction.ts +++ b/src/vs/editor/common/languages/enterAction.ts @@ -7,7 +7,8 @@ import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { IndentAction, CompleteEnterAction } from 'vs/editor/common/languages/languageConfiguration'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; -import { getIndentationAtPosition, getScopedLineTokens, ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { getIndentationAtPosition, ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IndentationContextProcessor } from 'vs/editor/common/languages/supports/indentationLineProcessor'; export function getEnterAction( autoIndent: EditorAutoIndentStrategy, @@ -15,33 +16,17 @@ export function getEnterAction( range: Range, languageConfigurationService: ILanguageConfigurationService ): CompleteEnterAction | null { - const scopedLineTokens = getScopedLineTokens(model, range.startLineNumber, range.startColumn); - const richEditSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId); + model.tokenization.forceTokenization(range.startLineNumber); + const languageId = model.getLanguageIdAtPosition(range.startLineNumber, range.startColumn); + const richEditSupport = languageConfigurationService.getLanguageConfiguration(languageId); if (!richEditSupport) { return null; } - - const scopedLineText = scopedLineTokens.getLineContent(); - const beforeEnterText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); - - // selection support - let afterEnterText: string; - if (range.isEmpty()) { - afterEnterText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - const endScopedLineTokens = getScopedLineTokens(model, range.endLineNumber, range.endColumn); - afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); - } - - let previousLineText = ''; - if (range.startLineNumber > 1 && scopedLineTokens.firstCharOffset === 0) { - // This is not the first line and the entire line belongs to this mode - const oneLineAboveScopedLineTokens = getScopedLineTokens(model, range.startLineNumber - 1); - if (oneLineAboveScopedLineTokens.languageId === scopedLineTokens.languageId) { - // The line above ends with text belonging to the same mode - previousLineText = oneLineAboveScopedLineTokens.getLineContent(); - } - } + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContextTokens = indentationContextProcessor.getProcessedTokenContextAroundRange(range); + const previousLineText = processedContextTokens.previousLineProcessedTokens.getLineContent(); + const beforeEnterText = processedContextTokens.beforeRangeProcessedTokens.getLineContent(); + const afterEnterText = processedContextTokens.afterRangeProcessedTokens.getLineContent(); const enterResult = richEditSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); if (!enterResult) { diff --git a/src/vs/editor/common/languages/languageConfigurationRegistry.ts b/src/vs/editor/common/languages/languageConfigurationRegistry.ts index d8598afe6fd..7ff5ddec8a6 100644 --- a/src/vs/editor/common/languages/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/languages/languageConfigurationRegistry.ts @@ -9,7 +9,6 @@ import * as strings from 'vs/base/common/strings'; import { ITextModel } from 'vs/editor/common/model'; import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from 'vs/editor/common/core/wordHelper'; import { EnterAction, FoldingRules, IAutoClosingPair, IndentationRule, LanguageConfiguration, AutoClosingPairs, CharacterPair, ExplicitLanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; -import { createScopedLineTokens, ScopedLineTokens } from 'vs/editor/common/languages/supports'; import { CharacterPairSupport } from 'vs/editor/common/languages/supports/characterPair'; import { BracketElectricCharacterSupport } from 'vs/editor/common/languages/supports/electricCharacter'; import { IndentRulesSupport } from 'vs/editor/common/languages/supports/indentRules'; @@ -181,13 +180,6 @@ export function getIndentationAtPosition(model: ITextModel, lineNumber: number, return indentation; } -export function getScopedLineTokens(model: ITextModel, lineNumber: number, columnNumber?: number): ScopedLineTokens { - model.tokenization.forceTokenization(lineNumber); - const lineTokens = model.tokenization.getLineTokens(lineNumber); - const column = (typeof columnNumber === 'undefined' ? model.getLineMaxColumn(lineNumber) - 1 : columnNumber - 1); - return createScopedLineTokens(lineTokens, column); -} - class ComposedLanguageConfiguration { private readonly _entries: LanguageConfigurationContribution[]; private _order: number; diff --git a/src/vs/editor/common/languages/supports.ts b/src/vs/editor/common/languages/supports.ts index 8fdfa17bb51..730fa2a1b73 100644 --- a/src/vs/editor/common/languages/supports.ts +++ b/src/vs/editor/common/languages/supports.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { IViewLineTokens, LineTokens } from 'vs/editor/common/tokens/lineTokens'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { ILanguageIdCodec } from 'vs/editor/common/languages'; export function createScopedLineTokens(context: LineTokens, offset: number): ScopedLineTokens { const tokenCount = context.getCount(); @@ -34,6 +35,7 @@ export function createScopedLineTokens(context: LineTokens, offset: number): Sco export class ScopedLineTokens { _scopedLineTokensBrand: void = undefined; + public readonly languageIdCodec: ILanguageIdCodec; public readonly languageId: string; private readonly _actual: LineTokens; private readonly _firstTokenIndex: number; @@ -55,6 +57,7 @@ export class ScopedLineTokens { this._lastTokenIndex = lastTokenIndex; this.firstCharOffset = firstCharOffset; this._lastCharOffset = lastCharOffset; + this.languageIdCodec = actual.languageIdCodec; } public getLineContent(): string { @@ -62,6 +65,10 @@ export class ScopedLineTokens { return actualLineContent.substring(this.firstCharOffset, this._lastCharOffset); } + public getLineLength(): number { + return this._lastCharOffset - this.firstCharOffset; + } + public getActualLineContentBefore(offset: number): string { const actualLineContent = this._actual.getLineContent(); return actualLineContent.substring(0, this.firstCharOffset + offset); @@ -78,6 +85,10 @@ export class ScopedLineTokens { public getStandardTokenType(tokenIndex: number): StandardTokenType { return this._actual.getStandardTokenType(tokenIndex + this._firstTokenIndex); } + + public toIViewLineTokens(): IViewLineTokens { + return this._actual.sliceAndInflate(this.firstCharOffset, this._lastCharOffset, 0); + } } const enum IgnoreBracketsInTokens { diff --git a/src/vs/editor/common/languages/supports/indentationLineProcessor.ts b/src/vs/editor/common/languages/supports/indentationLineProcessor.ts new file mode 100644 index 00000000000..919cb3cd4c8 --- /dev/null +++ b/src/vs/editor/common/languages/supports/indentationLineProcessor.ts @@ -0,0 +1,236 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as strings from 'vs/base/common/strings'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { createScopedLineTokens, ScopedLineTokens } from 'vs/editor/common/languages/supports'; +import { IVirtualModel } from 'vs/editor/common/languages/autoIndent'; +import { IViewLineTokens, LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { IndentRulesSupport } from 'vs/editor/common/languages/supports/indentRules'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { Position } from 'vs/editor/common/core/position'; + +/** + * This class is a wrapper class around {@link IndentRulesSupport}. + * It processes the lines by removing the language configuration brackets from the regex, string and comment tokens. + * It then calls into the {@link IndentRulesSupport} to validate the indentation conditions. + */ +export class ProcessedIndentRulesSupport { + + private readonly _indentRulesSupport: IndentRulesSupport; + private readonly _indentationLineProcessor: IndentationLineProcessor; + + constructor( + model: IVirtualModel, + indentRulesSupport: IndentRulesSupport, + languageConfigurationService: ILanguageConfigurationService + ) { + this._indentRulesSupport = indentRulesSupport; + this._indentationLineProcessor = new IndentationLineProcessor(model, languageConfigurationService); + } + + /** + * Apply the new indentation and return whether the indentation level should be increased after the given line number + */ + public shouldIncrease(lineNumber: number, newIndentation?: string): boolean { + const processedLine = this._indentationLineProcessor.getProcessedLine(lineNumber, newIndentation); + return this._indentRulesSupport.shouldIncrease(processedLine); + } + + /** + * Apply the new indentation and return whether the indentation level should be decreased after the given line number + */ + public shouldDecrease(lineNumber: number, newIndentation?: string): boolean { + const processedLine = this._indentationLineProcessor.getProcessedLine(lineNumber, newIndentation); + return this._indentRulesSupport.shouldDecrease(processedLine); + } + + /** + * Apply the new indentation and return whether the indentation level should remain unchanged at the given line number + */ + public shouldIgnore(lineNumber: number, newIndentation?: string): boolean { + const processedLine = this._indentationLineProcessor.getProcessedLine(lineNumber, newIndentation); + return this._indentRulesSupport.shouldIgnore(processedLine); + } + + /** + * Apply the new indentation and return whether the indentation level should increase on the line after the given line number + */ + public shouldIndentNextLine(lineNumber: number, newIndentation?: string): boolean { + const processedLine = this._indentationLineProcessor.getProcessedLine(lineNumber, newIndentation); + return this._indentRulesSupport.shouldIndentNextLine(processedLine); + } + +} + +/** + * This class fetches the processed text around a range which can be used for indentation evaluation. + * It returns: + * - The processed text before the given range and on the same start line + * - The processed text after the given range and on the same end line + * - The processed text on the previous line + */ +export class IndentationContextProcessor { + + private readonly model: ITextModel; + private readonly indentationLineProcessor: IndentationLineProcessor; + + constructor( + model: ITextModel, + languageConfigurationService: ILanguageConfigurationService + ) { + this.model = model; + this.indentationLineProcessor = new IndentationLineProcessor(model, languageConfigurationService); + } + + /** + * Returns the processed text, stripped from the language configuration brackets within the string, comment and regex tokens, around the given range + */ + getProcessedTokenContextAroundRange(range: Range): { + beforeRangeProcessedTokens: IViewLineTokens; + afterRangeProcessedTokens: IViewLineTokens; + previousLineProcessedTokens: IViewLineTokens; + } { + const beforeRangeProcessedTokens = this._getProcessedTokensBeforeRange(range); + const afterRangeProcessedTokens = this._getProcessedTokensAfterRange(range); + const previousLineProcessedTokens = this._getProcessedPreviousLineTokens(range); + return { beforeRangeProcessedTokens, afterRangeProcessedTokens, previousLineProcessedTokens }; + } + + private _getProcessedTokensBeforeRange(range: Range): IViewLineTokens { + this.model.tokenization.forceTokenization(range.startLineNumber); + const lineTokens = this.model.tokenization.getLineTokens(range.startLineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, range.startColumn - 1); + let slicedTokens: IViewLineTokens; + if (isLanguageDifferentFromLineStart(this.model, range.getStartPosition())) { + const columnIndexWithinScope = (range.startColumn - 1) - scopedLineTokens.firstCharOffset; + const firstCharacterOffset = scopedLineTokens.firstCharOffset; + const lastCharacterOffset = firstCharacterOffset + columnIndexWithinScope; + slicedTokens = lineTokens.sliceAndInflate(firstCharacterOffset, lastCharacterOffset, 0); + } else { + const columnWithinLine = range.startColumn - 1; + slicedTokens = lineTokens.sliceAndInflate(0, columnWithinLine, 0); + } + const processedTokens = this.indentationLineProcessor.getProcessedTokens(slicedTokens); + return processedTokens; + } + + private _getProcessedTokensAfterRange(range: Range): IViewLineTokens { + const position: Position = range.isEmpty() ? range.getStartPosition() : range.getEndPosition(); + this.model.tokenization.forceTokenization(position.lineNumber); + const lineTokens = this.model.tokenization.getLineTokens(position.lineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, position.column - 1); + const columnIndexWithinScope = position.column - 1 - scopedLineTokens.firstCharOffset; + const firstCharacterOffset = scopedLineTokens.firstCharOffset + columnIndexWithinScope; + const lastCharacterOffset = scopedLineTokens.firstCharOffset + scopedLineTokens.getLineLength(); + const slicedTokens = lineTokens.sliceAndInflate(firstCharacterOffset, lastCharacterOffset, 0); + const processedTokens = this.indentationLineProcessor.getProcessedTokens(slicedTokens); + return processedTokens; + } + + private _getProcessedPreviousLineTokens(range: Range): IViewLineTokens { + const getScopedLineTokensAtEndColumnOfLine = (lineNumber: number): ScopedLineTokens => { + this.model.tokenization.forceTokenization(lineNumber); + const lineTokens = this.model.tokenization.getLineTokens(lineNumber); + const endColumnOfLine = this.model.getLineMaxColumn(lineNumber) - 1; + const scopedLineTokensAtEndColumn = createScopedLineTokens(lineTokens, endColumnOfLine); + return scopedLineTokensAtEndColumn; + }; + + this.model.tokenization.forceTokenization(range.startLineNumber); + const lineTokens = this.model.tokenization.getLineTokens(range.startLineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, range.startColumn - 1); + const emptyTokens = LineTokens.createEmpty('', scopedLineTokens.languageIdCodec); + const previousLineNumber = range.startLineNumber - 1; + const isFirstLine = previousLineNumber === 0; + if (isFirstLine) { + return emptyTokens; + } + const canScopeExtendOnPreviousLine = scopedLineTokens.firstCharOffset === 0; + if (!canScopeExtendOnPreviousLine) { + return emptyTokens; + } + const scopedLineTokensAtEndColumnOfPreviousLine = getScopedLineTokensAtEndColumnOfLine(previousLineNumber); + const doesLanguageContinueOnPreviousLine = scopedLineTokens.languageId === scopedLineTokensAtEndColumnOfPreviousLine.languageId; + if (!doesLanguageContinueOnPreviousLine) { + return emptyTokens; + } + const previousSlicedLineTokens = scopedLineTokensAtEndColumnOfPreviousLine.toIViewLineTokens(); + const processedTokens = this.indentationLineProcessor.getProcessedTokens(previousSlicedLineTokens); + return processedTokens; + } +} + +/** + * This class performs the actual processing of the indentation lines. + * The brackets of the language configuration are removed from the regex, string and comment tokens. + */ +class IndentationLineProcessor { + + constructor( + private readonly model: IVirtualModel, + private readonly languageConfigurationService: ILanguageConfigurationService + ) { } + + /** + * Get the processed line for the given line number and potentially adjust the indentation level. + * Remove the language configuration brackets from the regex, string and comment tokens. + */ + getProcessedLine(lineNumber: number, newIndentation?: string): string { + const replaceIndentation = (line: string, newIndentation: string): string => { + const currentIndentation = strings.getLeadingWhitespace(line); + const adjustedLine = newIndentation + line.substring(currentIndentation.length); + return adjustedLine; + }; + + this.model.tokenization.forceTokenization?.(lineNumber); + const tokens = this.model.tokenization.getLineTokens(lineNumber); + let processedLine = this.getProcessedTokens(tokens).getLineContent(); + if (newIndentation !== undefined) { + processedLine = replaceIndentation(processedLine, newIndentation); + } + return processedLine; + } + + /** + * Process the line with the given tokens, remove the language configuration brackets from the regex, string and comment tokens. + */ + getProcessedTokens(tokens: IViewLineTokens): IViewLineTokens { + + const shouldRemoveBracketsFromTokenType = (tokenType: StandardTokenType): boolean => { + return tokenType === StandardTokenType.String + || tokenType === StandardTokenType.RegEx + || tokenType === StandardTokenType.Comment; + }; + + const languageId = tokens.getLanguageId(0); + const bracketsConfiguration = this.languageConfigurationService.getLanguageConfiguration(languageId).bracketsNew; + const bracketsRegExp = bracketsConfiguration.getBracketRegExp({ global: true }); + const textAndMetadata: { text: string; metadata: number }[] = []; + tokens.forEach((tokenIndex: number) => { + const tokenType = tokens.getStandardTokenType(tokenIndex); + let text = tokens.getTokenText(tokenIndex); + if (shouldRemoveBracketsFromTokenType(tokenType)) { + text = text.replace(bracketsRegExp, ''); + } + const metadata = tokens.getMetadata(tokenIndex); + textAndMetadata.push({ text, metadata }); + }); + const processedLineTokens = LineTokens.createFromTextAndMetadata(textAndMetadata, tokens.languageIdCodec); + return processedLineTokens; + } +} + +export function isLanguageDifferentFromLineStart(model: ITextModel, position: Position): boolean { + model.tokenization.forceTokenization(position.lineNumber); + const lineTokens = model.tokenization.getLineTokens(position.lineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, position.column - 1); + const doesScopeStartAtOffsetZero = scopedLineTokens.firstCharOffset === 0; + const isScopedLanguageEqualToFirstLanguageOnLine = lineTokens.getLanguageId(0) === scopedLineTokens.languageId; + const languageIsDifferentFromLineStart = !doesScopeStartAtOffsetZero && !isScopedLanguageEqualToFirstLanguageOnLine; + return languageIsDifferentFromLineStart; +} diff --git a/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts b/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts index a989e2f35e4..4989395b264 100644 --- a/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts +++ b/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { CachedFunction } from 'vs/base/common/cache'; +import { RegExpOptions } from 'vs/base/common/strings'; import { LanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; +import { createBracketOrRegExp } from 'vs/editor/common/languages/supports/richEditBrackets'; /** * Captures all bracket related configurations for a single language. @@ -91,6 +93,11 @@ export class LanguageBracketsConfiguration { public getBracketInfo(bracketText: string): BracketKind | undefined { return this.getOpeningBracketInfo(bracketText) || this.getClosingBracketInfo(bracketText); } + + public getBracketRegExp(options?: RegExpOptions): RegExp { + const brackets = Array.from([...this._openingBrackets.keys(), ...this._closingBrackets.keys()]); + return createBracketOrRegExp(brackets, options); + } } function filterValidBrackets(bracketPairs: [string, string][]): [string, string][] { diff --git a/src/vs/editor/common/languages/supports/richEditBrackets.ts b/src/vs/editor/common/languages/supports/richEditBrackets.ts index c6efd4ee7a7..abb30850466 100644 --- a/src/vs/editor/common/languages/supports/richEditBrackets.ts +++ b/src/vs/editor/common/languages/supports/richEditBrackets.ts @@ -408,9 +408,9 @@ function prepareBracketForRegExp(str: string): string { return (insertWordBoundaries ? `\\b${str}\\b` : str); } -function createBracketOrRegExp(pieces: string[]): RegExp { +export function createBracketOrRegExp(pieces: string[], options?: strings.RegExpOptions): RegExp { const regexStr = `(${pieces.map(prepareBracketForRegExp).join(')|(')})`; - return strings.createRegExp(regexStr, true); + return strings.createRegExp(regexStr, true, options); } const toReversedString = (function () { diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index e21aa7d600c..134234bbfe5 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1431,6 +1431,11 @@ export interface IReadonlyTextBuffer { getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[]; + + /** + * Get nearest chunk of text after `offset` in the text buffer. + */ + getNearestChunk(offset: number): string; } /** diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts index 470a16f009c..3d3fe2e3649 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts @@ -8,7 +8,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService, LanguageConfigurationServiceChangeEvent } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ignoreBracketsInToken } from 'vs/editor/common/languages/supports'; import { LanguageBracketsConfiguration } from 'vs/editor/common/languages/supports/languageBracketsConfiguration'; import { BracketsUtils, RichEditBracket, RichEditBrackets } from 'vs/editor/common/languages/supports/richEditBrackets'; @@ -36,19 +36,17 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai private readonly languageConfigurationService: ILanguageConfigurationService ) { super(); - - this._register( - this.languageConfigurationService.onDidChange(e => { - if (!e.languageId || this.bracketPairsTree.value?.object.didLanguageChange(e.languageId)) { - this.bracketPairsTree.clear(); - this.updateBracketPairsTree(); - } - }) - ); } //#region TextModel events + public handleLanguageConfigurationServiceChange(e: LanguageConfigurationServiceChangeEvent): void { + if (!e.languageId || this.bracketPairsTree.value?.object.didLanguageChange(e.languageId)) { + this.bracketPairsTree.clear(); + this.updateBracketPairsTree(); + } + } + public handleDidChangeOptions(e: IModelOptionsChangedEvent): void { this.bracketPairsTree.clear(); this.updateBracketPairsTree(); diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index b75d0d75a70..24f90651f95 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -666,6 +666,27 @@ export class PieceTreeBase { return this._getCharCode(nodePos); } + public getNearestChunk(offset: number): string { + const nodePos = this.nodeAt(offset); + if (nodePos.remainder === nodePos.node.piece.length) { + // the offset is at the head of next node. + const matchingNode = nodePos.node.next(); + if (!matchingNode || matchingNode === SENTINEL) { + return ''; + } + + const buffer = this._buffers[matchingNode.piece.bufferIndex]; + const startOffset = this.offsetInBuffer(matchingNode.piece.bufferIndex, matchingNode.piece.start); + return buffer.buffer.substring(startOffset, startOffset + matchingNode.piece.length); + } else { + const buffer = this._buffers[nodePos.node.piece.bufferIndex]; + const startOffset = this.offsetInBuffer(nodePos.node.piece.bufferIndex, nodePos.node.piece.start); + const targetOffset = startOffset + nodePos.remainder; + const targetEnd = startOffset + nodePos.node.piece.length; + return buffer.buffer.substring(targetOffset, targetEnd); + } + } + public findMatchesInNode(node: TreeNode, searcher: Searcher, startLineNumber: number, startColumn: number, startCursor: BufferCursor, endCursor: BufferCursor, searchData: SearchData, captureMatches: boolean, limitResultCount: number, resultLen: number, result: FindMatch[]) { const buffer = this._buffers[node.piece.bufferIndex]; const startOffsetInBuffer = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start); diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 12d7e0b0981..a369298c0c9 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -167,6 +167,10 @@ export class PieceTreeTextBuffer extends Disposable implements ITextBuffer { return this.getValueLengthInRange(range, eol); } + public getNearestChunk(offset: number): string { + return this._pieceTree.getNearestChunk(offset); + } + public getLength(): number { return this._pieceTree.getLength(); } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 626217e8bff..97b5a483fc3 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -381,6 +381,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati })); this._languageService.requestRichLanguageFeatures(languageId); + + this._register(this._languageConfigurationService.onDidChange(e => { + this._bracketPairs.handleLanguageConfigurationServiceChange(e); + this._tokenizationTextModelPart.handleLanguageConfigurationServiceChange(e); + })); } public override dispose(): void { @@ -413,7 +418,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private _assertNotDisposed(): void { if (this._isDisposed) { - throw new Error('Model is disposed!'); + throw new BugIndicatingError('Model is disposed!'); } } diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index 804f63c6a28..40c6c921afc 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -17,7 +17,7 @@ import { IWordAtPosition, getWordAtText } from 'vs/editor/common/core/wordHelper import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { IBackgroundTokenizationStore, IBackgroundTokenizer, ILanguageIdCodec, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService, LanguageConfigurationServiceChangeEvent, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IAttachedView } from 'vs/editor/common/model'; import { BracketPairsTextModelPart } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl'; import { AttachedViews, IAttachedViewState, TextModel } from 'vs/editor/common/model/textModel'; @@ -56,12 +56,6 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz ) { super(); - this._register(this._languageConfigurationService.onDidChange(e => { - if (e.affects(this._languageId)) { - this._onDidChangeLanguageConfiguration.fire({}); - } - })); - this._register(this.grammarTokens.onDidChangeTokens(e => { this._emitModelTokensChangedEvent(e); })); @@ -77,6 +71,12 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz || this._onDidChangeTokens.hasListeners()); } + public handleLanguageConfigurationServiceChange(e: LanguageConfigurationServiceChangeEvent): void { + if (e.affects(this._languageId)) { + this._onDidChangeLanguageConfiguration.fire({}); + } + } + public handleDidChangeContent(e: IModelContentChangedEvent): void { if (e.isFlush) { this._semanticTokens.flush(); diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 3608f3042a8..52a1e2633e6 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -123,5 +123,5 @@ function detectLanguageId(modelService: IModelService, languageService: ILanguag } function cssEscape(str: string): string { - return str.replace(/[\11\12\14\15\40]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. + return str.replace(/[\x11\x12\x14\x15\x40]/g, '/'); // HTML class names can not contain certain whitespace characters, use / instead, which doesn't exist in file names. } diff --git a/src/vs/editor/common/services/semanticTokensProviderStyling.ts b/src/vs/editor/common/services/semanticTokensProviderStyling.ts index f248e0e23c2..1bb2e0d6ed1 100644 --- a/src/vs/editor/common/services/semanticTokensProviderStyling.ts +++ b/src/vs/editor/common/services/semanticTokensProviderStyling.ts @@ -14,6 +14,8 @@ const enum SemanticTokensProviderStylingConstants { NO_STYLING = 0b01111111111111111111111111111111 } +const ENABLE_TRACE = false; + export class SemanticTokensProviderStyling { private readonly _hashTable: HashTable; @@ -36,7 +38,7 @@ export class SemanticTokensProviderStyling { let metadata: number; if (entry) { metadata = entry.metadata; - if (this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling [CACHED] ${tokenTypeIndex} / ${tokenModifierSet}: foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`); } } else { @@ -50,7 +52,7 @@ export class SemanticTokensProviderStyling { } modifierSet = modifierSet >> 1; } - if (modifierSet > 0 && this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && modifierSet > 0 && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling: unknown token modifier index: ${tokenModifierSet.toString(2)} for legend: ${JSON.stringify(this._legend.tokenModifiers)}`); tokenModifiers.push('not-in-legend'); } @@ -86,7 +88,7 @@ export class SemanticTokensProviderStyling { } } } else { - if (this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling: unknown token type index: ${tokenTypeIndex} for legend: ${JSON.stringify(this._legend.tokenTypes)}`); } metadata = SemanticTokensProviderStylingConstants.NO_STYLING; @@ -94,7 +96,7 @@ export class SemanticTokensProviderStyling { } this._hashTable.add(tokenTypeIndex, tokenModifierSet, encodedLanguageId, metadata); - if (this._logService.getLevel() === LogLevel.Trace) { + if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling ${tokenTypeIndex} (${tokenType}) / ${tokenModifierSet} (${tokenModifiers.join(' ')}): foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`); } } diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 4c5c84ee0f6..7cd16daae8b 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -261,68 +261,69 @@ export enum EditorOption { pasteAs = 85, parameterHints = 86, peekWidgetDefaultFocus = 87, - definitionLinkOpensInPeek = 88, - quickSuggestions = 89, - quickSuggestionsDelay = 90, - readOnly = 91, - readOnlyMessage = 92, - renameOnType = 93, - renderControlCharacters = 94, - renderFinalNewline = 95, - renderLineHighlight = 96, - renderLineHighlightOnlyWhenFocus = 97, - renderValidationDecorations = 98, - renderWhitespace = 99, - revealHorizontalRightPadding = 100, - roundedSelection = 101, - rulers = 102, - scrollbar = 103, - scrollBeyondLastColumn = 104, - scrollBeyondLastLine = 105, - scrollPredominantAxis = 106, - selectionClipboard = 107, - selectionHighlight = 108, - selectOnLineNumbers = 109, - showFoldingControls = 110, - showUnused = 111, - snippetSuggestions = 112, - smartSelect = 113, - smoothScrolling = 114, - stickyScroll = 115, - stickyTabStops = 116, - stopRenderingLineAfter = 117, - suggest = 118, - suggestFontSize = 119, - suggestLineHeight = 120, - suggestOnTriggerCharacters = 121, - suggestSelection = 122, - tabCompletion = 123, - tabIndex = 124, - unicodeHighlighting = 125, - unusualLineTerminators = 126, - useShadowDOM = 127, - useTabStops = 128, - wordBreak = 129, - wordSegmenterLocales = 130, - wordSeparators = 131, - wordWrap = 132, - wordWrapBreakAfterCharacters = 133, - wordWrapBreakBeforeCharacters = 134, - wordWrapColumn = 135, - wordWrapOverride1 = 136, - wordWrapOverride2 = 137, - wrappingIndent = 138, - wrappingStrategy = 139, - showDeprecated = 140, - inlayHints = 141, - editorClassName = 142, - pixelRatio = 143, - tabFocusMode = 144, - layoutInfo = 145, - wrappingInfo = 146, - defaultColorDecorators = 147, - colorDecoratorsActivatedOn = 148, - inlineCompletionsAccessibilityVerbose = 149 + placeholder = 88, + definitionLinkOpensInPeek = 89, + quickSuggestions = 90, + quickSuggestionsDelay = 91, + readOnly = 92, + readOnlyMessage = 93, + renameOnType = 94, + renderControlCharacters = 95, + renderFinalNewline = 96, + renderLineHighlight = 97, + renderLineHighlightOnlyWhenFocus = 98, + renderValidationDecorations = 99, + renderWhitespace = 100, + revealHorizontalRightPadding = 101, + roundedSelection = 102, + rulers = 103, + scrollbar = 104, + scrollBeyondLastColumn = 105, + scrollBeyondLastLine = 106, + scrollPredominantAxis = 107, + selectionClipboard = 108, + selectionHighlight = 109, + selectOnLineNumbers = 110, + showFoldingControls = 111, + showUnused = 112, + snippetSuggestions = 113, + smartSelect = 114, + smoothScrolling = 115, + stickyScroll = 116, + stickyTabStops = 117, + stopRenderingLineAfter = 118, + suggest = 119, + suggestFontSize = 120, + suggestLineHeight = 121, + suggestOnTriggerCharacters = 122, + suggestSelection = 123, + tabCompletion = 124, + tabIndex = 125, + unicodeHighlighting = 126, + unusualLineTerminators = 127, + useShadowDOM = 128, + useTabStops = 129, + wordBreak = 130, + wordSegmenterLocales = 131, + wordSeparators = 132, + wordWrap = 133, + wordWrapBreakAfterCharacters = 134, + wordWrapBreakBeforeCharacters = 135, + wordWrapColumn = 136, + wordWrapOverride1 = 137, + wordWrapOverride2 = 138, + wrappingIndent = 139, + wrappingStrategy = 140, + showDeprecated = 141, + inlayHints = 142, + editorClassName = 143, + pixelRatio = 144, + tabFocusMode = 145, + layoutInfo = 146, + wrappingInfo = 147, + defaultColorDecorators = 148, + colorDecoratorsActivatedOn = 149, + inlineCompletionsAccessibilityVerbose = 150 } /** diff --git a/src/vs/editor/common/tokens/lineTokens.ts b/src/vs/editor/common/tokens/lineTokens.ts index bdcda7cb180..67cdf815001 100644 --- a/src/vs/editor/common/tokens/lineTokens.ts +++ b/src/vs/editor/common/tokens/lineTokens.ts @@ -5,10 +5,14 @@ import { ILanguageIdCodec } from 'vs/editor/common/languages'; import { FontStyle, ColorId, StandardTokenType, MetadataConsts, TokenMetadata, ITokenPresentation } from 'vs/editor/common/encodedTokenAttributes'; +import { IPosition } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; export interface IViewLineTokens { + languageIdCodec: ILanguageIdCodec; equals(other: IViewLineTokens): boolean; getCount(): number; + getStandardTokenType(tokenIndex: number): StandardTokenType; getForeground(tokenIndex: number): ColorId; getEndOffset(tokenIndex: number): number; getClassName(tokenIndex: number): string; @@ -18,6 +22,8 @@ export interface IViewLineTokens { getLineContent(): string; getMetadata(tokenIndex: number): number; getLanguageId(tokenIndex: number): string; + getTokenText(tokenIndex: number): string; + forEach(callback: (tokenIndex: number) => void): void; } export class LineTokens implements IViewLineTokens { @@ -26,7 +32,8 @@ export class LineTokens implements IViewLineTokens { private readonly _tokens: Uint32Array; private readonly _tokensCount: number; private readonly _text: string; - private readonly _languageIdCodec: ILanguageIdCodec; + + public readonly languageIdCodec: ILanguageIdCodec; public static defaultTokenMetadata = ( (FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET) @@ -44,11 +51,23 @@ export class LineTokens implements IViewLineTokens { return new LineTokens(tokens, lineContent, decoder); } + public static createFromTextAndMetadata(data: { text: string; metadata: number }[], decoder: ILanguageIdCodec): LineTokens { + let offset: number = 0; + let fullText: string = ''; + const tokens = new Array(); + for (const { text, metadata } of data) { + tokens.push(offset + text.length, metadata); + offset += text.length; + fullText += text; + } + return new LineTokens(new Uint32Array(tokens), fullText, decoder); + } + constructor(tokens: Uint32Array, text: string, decoder: ILanguageIdCodec) { this._tokens = tokens; this._tokensCount = (this._tokens.length >>> 1); this._text = text; - this._languageIdCodec = decoder; + this.languageIdCodec = decoder; } public equals(other: IViewLineTokens): boolean { @@ -98,7 +117,7 @@ export class LineTokens implements IViewLineTokens { public getLanguageId(tokenIndex: number): string { const metadata = this._tokens[(tokenIndex << 1) + 1]; const languageId = TokenMetadata.getLanguageId(metadata); - return this._languageIdCodec.decodeLanguageId(languageId); + return this.languageIdCodec.decodeLanguageId(languageId); } public getStandardTokenType(tokenIndex: number): StandardTokenType { @@ -225,7 +244,21 @@ export class LineTokens implements IViewLineTokens { } } - return new LineTokens(new Uint32Array(newTokens), text, this._languageIdCodec); + return new LineTokens(new Uint32Array(newTokens), text, this.languageIdCodec); + } + + public getTokenText(tokenIndex: number): string { + const startOffset = this.getStartOffset(tokenIndex); + const endOffset = this.getEndOffset(tokenIndex); + const text = this._text.substring(startOffset, endOffset); + return text; + } + + public forEach(callback: (tokenIndex: number) => void): void { + const tokenCount = this.getCount(); + for (let tokenIndex = 0; tokenIndex < tokenCount; tokenIndex++) { + callback(tokenIndex); + } } } @@ -239,12 +272,15 @@ class SliceLineTokens implements IViewLineTokens { private readonly _firstTokenIndex: number; private readonly _tokensCount: number; + public readonly languageIdCodec: ILanguageIdCodec; + constructor(source: LineTokens, startOffset: number, endOffset: number, deltaOffset: number) { this._source = source; this._startOffset = startOffset; this._endOffset = endOffset; this._deltaOffset = deltaOffset; this._firstTokenIndex = source.findTokenIndexAtOffset(startOffset); + this.languageIdCodec = source.languageIdCodec; this._tokensCount = 0; for (let i = this._firstTokenIndex, len = source.getCount(); i < len; i++) { @@ -284,6 +320,10 @@ class SliceLineTokens implements IViewLineTokens { return this._tokensCount; } + public getStandardTokenType(tokenIndex: number): StandardTokenType { + return this._source.getStandardTokenType(this._firstTokenIndex + tokenIndex); + } + public getForeground(tokenIndex: number): ColorId { return this._source.getForeground(this._firstTokenIndex + tokenIndex); } @@ -308,4 +348,36 @@ class SliceLineTokens implements IViewLineTokens { public findTokenIndexAtOffset(offset: number): number { return this._source.findTokenIndexAtOffset(offset + this._startOffset - this._deltaOffset) - this._firstTokenIndex; } + + public getTokenText(tokenIndex: number): string { + const adjustedTokenIndex = this._firstTokenIndex + tokenIndex; + const tokenStartOffset = this._source.getStartOffset(adjustedTokenIndex); + const tokenEndOffset = this._source.getEndOffset(adjustedTokenIndex); + let text = this._source.getTokenText(adjustedTokenIndex); + if (tokenStartOffset < this._startOffset) { + text = text.substring(this._startOffset - tokenStartOffset); + } + if (tokenEndOffset > this._endOffset) { + text = text.substring(0, text.length - (tokenEndOffset - this._endOffset)); + } + return text; + } + + public forEach(callback: (tokenIndex: number) => void): void { + for (let tokenIndex = 0; tokenIndex < this.getCount(); tokenIndex++) { + callback(tokenIndex); + } + } +} + +export function getStandardTokenTypeAtPosition(model: ITextModel, position: IPosition): StandardTokenType | undefined { + const lineNumber = position.lineNumber; + if (!model.tokenization.isCheapToTokenize(lineNumber)) { + return undefined; + } + model.tokenization.forceTokenization(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); + const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + const tokenType = lineTokens.getStandardTokenType(tokenIndex); + return tokenType; } diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index c5af99cca77..50366d54958 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -71,6 +71,7 @@ export class ViewModel extends Disposable implements IViewModel { private readonly languageConfigurationService: ILanguageConfigurationService, private readonly _themeService: IThemeService, private readonly _attachedView: IAttachedView, + private readonly _transactionalTarget: IBatchableTarget, ) { super(); @@ -1102,12 +1103,14 @@ export class ViewModel extends Disposable implements IViewModel { //#endregion private _withViewEventsCollector(callback: (eventsCollector: ViewModelEventsCollector) => T): T { - try { - const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); - return callback(eventsCollector); - } finally { - this._eventDispatcher.endEmitViewEvents(); - } + return this._transactionalTarget.batchChanges(() => { + try { + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + return callback(eventsCollector); + } finally { + this._eventDispatcher.endEmitViewEvents(); + } + }); } public batchEvents(callback: () => void): void { @@ -1127,6 +1130,13 @@ export class ViewModel extends Disposable implements IViewModel { } } +export interface IBatchableTarget { + /** + * Allows the target to apply the changes introduced by the callback in a batch. + */ + batchChanges(cb: () => T): T; +} + class ViewportStart implements IDisposable { public static create(model: ITextModel): ViewportStart { diff --git a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts index ffd9e3240dd..b3530eaa888 100644 --- a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts @@ -23,7 +23,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', { dark: '#A0A0A0', light: '#A0A0A0', hcDark: '#A0A0A0', hcLight: '#A0A0A0' }, nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.')); +const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', '#A0A0A0', nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.')); class JumpToBracketAction extends EditorAction { constructor() { diff --git a/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts index 289fe8aa96a..7a5f7e106e7 100644 --- a/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts +++ b/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 65dc839da2c..612fd4d2923 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -39,8 +39,6 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; import { CodeActionModel, CodeActionsState } from 'vs/editor/contrib/codeAction/browser/codeActionModel'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; - interface IActionShowOptions { readonly includeDisabledActions?: boolean; @@ -79,8 +77,7 @@ export class CodeActionController extends Disposable implements IEditorContribut @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -107,29 +104,6 @@ export class CodeActionController extends Disposable implements IEditorContribut } private async showCodeActionsFromLightbulb(actions: CodeActionSet, at: IAnchor | IPosition): Promise { - - // Telemetry for showing code actions from lightbulb. Shows us how often it was clicked. - type ShowCodeActionListEvent = { - codeActionListLength: number; - codeActions: string[]; - codeActionProviders: string[]; - }; - - type ShowListEventClassification = { - codeActionListLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The length of the code action list from the lightbulb widget.' }; - codeActions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The title of code actions in this menu.' }; - codeActionProviders: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider of code actions in this menu.' }; - owner: 'justschen'; - comment: 'Event used to gain insights into what code actions are being shown'; - }; - - this._telemetryService.publicLog2('codeAction.showCodeActionsFromLightbulb', { - codeActionListLength: actions.validActions.length, - codeActions: actions.validActions.map(action => action.action.title), - codeActionProviders: actions.validActions.map(action => action.provider?.displayName ?? ''), - }); - - if (actions.allAIFixes && actions.validActions.length === 1) { const actionItem = actions.validActions[0]; const command = actionItem.action.command; @@ -312,28 +286,6 @@ export class CodeActionController extends Disposable implements IEditorContribut onHide: (didCancel?) => { this._editor?.focus(); currentDecorations.clear(); - // Telemetry for showing code actions here. only log on `showLightbulb`. Logs when code action list is quit out. - if (options.fromLightbulb && didCancel !== undefined) { - type ShowCodeActionListEvent = { - codeActionListLength: number; - didCancel: boolean; - codeActions: string[]; - }; - - type ShowListEventClassification = { - codeActionListLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The length of the code action list when quit out. Can be from any code action menu.' }; - didCancel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code action was cancelled or selected.' }; - codeActions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What code actions were available when cancelled.' }; - owner: 'justschen'; - comment: 'Event used to gain insights into how many valid code actions are being shown'; - }; - - this._telemetryService.publicLog2('codeAction.showCodeActionList.onHide', { - codeActionListLength: actions.validActions.length, - didCancel: didCancel, - codeActions: actions.validActions.map(action => action.action.title), - }); - } }, onHover: async (action: CodeActionItem, token: CancellationToken) => { if (token.isCancellationRequested) { diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css index ea05c8574b7..cbadb2348ef 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.css @@ -41,6 +41,5 @@ width: 100%; height: 100%; opacity: 0.3; - background-color: var(--vscode-editor-background); z-index: 1; } diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts index 94518efd415..45fc9aa9cb7 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts @@ -17,7 +17,6 @@ import { computeIndentLevel } from 'vs/editor/common/model/utils'; import { autoFixCommandId, quickFixCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionSet, CodeActionTrigger } from 'vs/editor/contrib/codeAction/common/types'; import * as nls from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; namespace LightBulbState { @@ -62,8 +61,7 @@ export class LightBulbWidget extends Disposable implements IContentWidget { constructor( private readonly _editor: ICodeEditor, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ICommandService commandService: ICommandService + @IKeybindingService private readonly _keybindingService: IKeybindingService ) { super(); @@ -173,8 +171,27 @@ export class LightBulbWidget extends Disposable implements IContentWidget { let effectiveLineNumber = lineNumber; let effectiveColumnNumber = 1; if (!lineHasSpace) { + + // Checks if line is empty or starts with any amount of whitespace + const isLineEmptyOrIndented = (lineNumber: number): boolean => { + const lineContent = model.getLineContent(lineNumber); + return /^\s*$|^\s+/.test(lineContent) || lineContent.length <= effectiveColumnNumber; + }; + if (lineNumber > 1 && !isFolded(lineNumber - 1)) { - effectiveLineNumber -= 1; + const lineCount = model.getLineCount(); + const endLine = lineNumber === lineCount; + const prevLineEmptyOrIndented = lineNumber > 1 && isLineEmptyOrIndented(lineNumber - 1); + const nextLineEmptyOrIndented = !endLine && isLineEmptyOrIndented(lineNumber + 1); + const currLineEmptyOrIndented = isLineEmptyOrIndented(lineNumber); + const notEmpty = !nextLineEmptyOrIndented && !prevLineEmptyOrIndented; + + // check above and below. if both are blocked, display lightbulb below. + if (prevLineEmptyOrIndented || endLine || (notEmpty && !currLineEmptyOrIndented)) { + effectiveLineNumber -= 1; + } else if (nextLineEmptyOrIndented || (notEmpty && currLineEmptyOrIndented)) { + effectiveLineNumber += 1; + } } else if ((lineNumber < model.getLineCount()) && !isFolded(lineNumber + 1)) { effectiveLineNumber += 1; } else if (column * fontInfo.spaceWidth < 22) { diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts index c1783a39f11..a98da2b0e0d 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeAction.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts index 641f1491d6f..664a36b2dca 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeActionKeybindingResolver.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyCodeChord } from 'vs/base/common/keybindings'; import { KeyCode } from 'vs/base/common/keyCodes'; import { OperatingSystem } from 'vs/base/common/platform'; @@ -101,4 +101,3 @@ function createCodeActionKeybinding(keycode: KeyCode, command: string, commandAr null, false); } - diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts index 71d6df33c0b..5946fb24f50 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { promiseWithResolvers } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { assertType } from 'vs/base/common/types'; diff --git a/src/vs/editor/contrib/codelens/browser/codeLensCache.ts b/src/vs/editor/contrib/codelens/browser/codeLensCache.ts index 67fa3b8009c..f7666b8c246 100644 --- a/src/vs/editor/contrib/codelens/browser/codeLensCache.ts +++ b/src/vs/editor/contrib/codelens/browser/codeLensCache.ts @@ -61,18 +61,17 @@ export class CodeLensCache implements ICodeLensCache { this._deserialize(raw); // store lens data on shutdown - Event.once(storageService.onWillSaveState)(e => { - if (e.reason === WillSaveStateReason.SHUTDOWN) { - storageService.store(key, this._serialize(), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } + const onWillSaveStateBecauseOfShutdown = Event.filter(storageService.onWillSaveState, e => e.reason === WillSaveStateReason.SHUTDOWN); + Event.once(onWillSaveStateBecauseOfShutdown)(e => { + storageService.store(key, this._serialize(), StorageScope.WORKSPACE, StorageTarget.MACHINE); }); } put(model: ITextModel, data: CodeLensModel): void { // create a copy of the model that is without command-ids // but with comand-labels - const copyItems = data.lenses.map(item => { - return { + const copyItems = data.lenses.map((item): CodeLens => { + return { range: item.symbol.range, command: item.symbol.command && { id: '', title: item.symbol.command?.title }, }; diff --git a/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts b/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts index e37a442f643..32956ec08da 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts @@ -6,7 +6,7 @@ import { AsyncIterableObject } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; @@ -16,11 +16,12 @@ import { getColorPresentations, getColors } from 'vs/editor/contrib/colorPicker/ import { ColorDetector } from 'vs/editor/contrib/colorPicker/browser/colorDetector'; import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/browser/colorPickerModel'; import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/browser/colorPickerWidget'; -import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import { Dimension } from 'vs/base/browser/dom'; +import * as nls from 'vs/nls'; export class ColorHover implements IHoverPart { @@ -50,6 +51,8 @@ export class ColorHoverParticipant implements IEditorHoverParticipant { + const renderedPart = renderHoverParts(this, this._editor, this._themeService, hoverParts, context); + if (!renderedPart) { + return new RenderedHoverParts([]); + } + this._colorPicker = renderedPart.colorPicker; + const renderedHoverPart: IRenderedHoverPart = { + hoverPart: renderedPart.hoverPart, + hoverElement: this._colorPicker.domNode, + dispose() { renderedPart.disposables.dispose(); } + }; + return new RenderedHoverParts([renderedHoverPart]); + } + + public getAccessibleContent(hoverPart: ColorHover): string { + return nls.localize('hoverAccessibilityColorParticipant', 'There is a color picker here.'); + } + + public handleResize(): void { + this._colorPicker?.layout(); + } + + public isColorPickerVisible(): boolean { + return !!this._colorPicker; } } @@ -146,7 +171,7 @@ export class StandaloneColorPickerParticipant { } } - public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: ColorHover[] | StandaloneColorPickerHover[]): IDisposable { + public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: StandaloneColorPickerHover[]): { disposables: IDisposable; hoverPart: StandaloneColorPickerHover; colorPicker: ColorPickerWidget } | undefined { return renderHoverParts(this, this._editor, this._themeService, hoverParts, context); } @@ -178,9 +203,9 @@ async function _createColorHover(participant: ColorHoverParticipant | Standalone } } -function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPickerParticipant, editor: ICodeEditor, themeService: IThemeService, hoverParts: ColorHover[] | StandaloneColorPickerHover[], context: IEditorHoverRenderContext) { +function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPickerParticipant, editor: ICodeEditor, themeService: IThemeService, hoverParts: T[], context: IEditorHoverRenderContext): { hoverPart: T; colorPicker: ColorPickerWidget; disposables: DisposableStore } | undefined { if (hoverParts.length === 0 || !editor.hasModel()) { - return Disposable.None; + return undefined; } if (context.setMinimumDimensions) { const minimumHeight = editor.getOption(EditorOption.lineHeight) + 8; @@ -191,13 +216,12 @@ function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPi const colorHover = hoverParts[0]; const editorModel = editor.getModel(); const model = colorHover.model; - const widget = disposables.add(new ColorPickerWidget(context.fragment, model, editor.getOption(EditorOption.pixelRatio), themeService, participant instanceof StandaloneColorPickerParticipant)); - context.setColorPicker(widget); + const colorPicker = disposables.add(new ColorPickerWidget(context.fragment, model, editor.getOption(EditorOption.pixelRatio), themeService, participant instanceof StandaloneColorPickerParticipant)); let editorUpdatedByColorPicker = false; let range = new Range(colorHover.range.startLineNumber, colorHover.range.startColumn, colorHover.range.endLineNumber, colorHover.range.endColumn); if (participant instanceof StandaloneColorPickerParticipant) { - const color = hoverParts[0].model.color; + const color = colorHover.model.color; participant.color = color; _updateColorPresentations(editorModel, model, color, range, colorHover); disposables.add(model.onColorFlushed((color: Color) => { @@ -221,7 +245,7 @@ function renderHoverParts(participant: ColorHoverParticipant | StandaloneColorPi editor.focus(); } })); - return disposables; + return { hoverPart: colorHover, colorPicker, disposables }; } function _updateEditorModel(editor: IActiveCodeEditor, range: Range, model: ColorPickerModel): Range { diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts index 7688defca5f..b911fe5b0f5 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts @@ -468,6 +468,7 @@ export class InsertButton extends Disposable { export class ColorPickerWidget extends Widget implements IEditorHoverColorPickerWidget { private static readonly ID = 'editor.contrib.colorPickerWidget'; + private readonly _domNode: HTMLElement; body: ColorPickerBody; header: ColorPickerHeader; @@ -477,11 +478,11 @@ export class ColorPickerWidget extends Widget implements IEditorHoverColorPicker this._register(PixelRatio.getInstance(dom.getWindow(container)).onDidChange(() => this.layout())); - const element = $('.colorpicker-widget'); - container.appendChild(element); + this._domNode = $('.colorpicker-widget'); + container.appendChild(this._domNode); - this.header = this._register(new ColorPickerHeader(element, this.model, themeService, standaloneColorPicker)); - this.body = this._register(new ColorPickerBody(element, this.model, this.pixelRatio, standaloneColorPicker)); + this.header = this._register(new ColorPickerHeader(this._domNode, this.model, themeService, standaloneColorPicker)); + this.body = this._register(new ColorPickerBody(this._domNode, this.model, this.pixelRatio, standaloneColorPicker)); } getId(): string { @@ -491,4 +492,8 @@ export class ColorPickerWidget extends Widget implements IEditorHoverColorPicker layout(): void { this.body.layout(); } + + get domNode(): HTMLElement { + return this._domNode; + } } diff --git a/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts index 4c8cf865269..424448611b1 100644 --- a/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/browser/standaloneColorPickerWidget.ts @@ -12,7 +12,7 @@ import { StandaloneColorPickerHover, StandaloneColorPickerParticipant } from 'vs import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/contentHoverStatusBar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ColorPickerWidget, InsertButton } from 'vs/editor/contrib/colorPicker/browser/colorPickerWidget'; +import { InsertButton } from 'vs/editor/contrib/colorPicker/browser/colorPickerWidget'; import { Emitter } from 'vs/base/common/event'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IColorInformation } from 'vs/editor/common/languages'; @@ -215,42 +215,42 @@ export class StandaloneColorPickerWidget extends Disposable implements IContentW private _render(colorHover: StandaloneColorPickerHover, foundInEditor: boolean) { const fragment = document.createDocumentFragment(); const statusBar = this._register(new EditorHoverStatusBar(this._keybindingService)); - let colorPickerWidget: ColorPickerWidget | undefined; const context: IEditorHoverRenderContext = { fragment, statusBar, - setColorPicker: (widget: ColorPickerWidget) => colorPickerWidget = widget, onContentsChanged: () => { }, hide: () => this.hide() }; this._colorHover = colorHover; - this._register(this._standaloneColorPickerParticipant.renderHoverParts(context, [colorHover])); - if (colorPickerWidget === undefined) { + const renderedHoverPart = this._standaloneColorPickerParticipant.renderHoverParts(context, [colorHover]); + if (!renderedHoverPart) { return; } + this._register(renderedHoverPart.disposables); + const colorPicker = renderedHoverPart.colorPicker; this._body.classList.add('standalone-colorpicker-body'); this._body.style.maxHeight = Math.max(this._editor.getLayoutInfo().height / 4, 250) + 'px'; this._body.style.maxWidth = Math.max(this._editor.getLayoutInfo().width * 0.66, 500) + 'px'; this._body.tabIndex = 0; this._body.appendChild(fragment); - colorPickerWidget.layout(); + colorPicker.layout(); - const colorPickerBody = colorPickerWidget.body; + const colorPickerBody = colorPicker.body; const saturationBoxWidth = colorPickerBody.saturationBox.domNode.clientWidth; const widthOfOriginalColorBox = colorPickerBody.domNode.clientWidth - saturationBoxWidth - CLOSE_BUTTON_WIDTH - PADDING; - const enterButton: InsertButton | null = colorPickerWidget.body.enterButton; + const enterButton: InsertButton | null = colorPicker.body.enterButton; enterButton?.onClicked(() => { this.updateEditor(); this.hide(); }); - const colorPickerHeader = colorPickerWidget.header; + const colorPickerHeader = colorPicker.header; const pickedColorNode = colorPickerHeader.pickedColorNode; pickedColorNode.style.width = saturationBoxWidth + PADDING + 'px'; const originalColorNode = colorPickerHeader.originalColorNode; originalColorNode.style.width = widthOfOriginalColorBox + 'px'; - const closeButton = colorPickerWidget.header.closeButton; + const closeButton = colorPicker.header.closeButton; closeButton?.onClicked(() => { this.hide(); }); diff --git a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts index ff394a835bb..f40f7b1e252 100644 --- a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Selection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts index 95609201da3..650a15e2e36 100644 --- a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts @@ -160,9 +160,7 @@ export class ContextMenuController implements IEditorContribution { const result: IAction[] = []; // get menu groups - const menu = this._menuService.createMenu(menuId, this._contextKeyService); - const groups = menu.getActions({ arg: model.uri }); - menu.dispose(); + const groups = this._menuService.getMenuActions(menuId, this._contextKeyService, { arg: model.uri }); // translate them into other actions for (const group of groups) { @@ -227,7 +225,7 @@ export class ContextMenuController implements IEditorContribution { // Show menu this._contextMenuIsBeingShownCount++; this._contextMenuService.showContextMenu({ - domForShadowRoot: useShadowDOM ? this._editor.getDomNode() : undefined, + domForShadowRoot: useShadowDOM ? this._editor.getOverflowWidgetsDomNode() ?? this._editor.getDomNode() : undefined, getAnchor: () => anchor, diff --git a/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts b/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts index 3cecf3ee8eb..d0e3423a47b 100644 --- a/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts +++ b/src/vs/editor/contrib/cursorUndo/test/browser/cursorUndo.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreEditingCommands, CoreNavigationCommands } from 'vs/editor/browser/coreCommands'; import { Selection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts b/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts index 194b8ee4f16..185d30db3e6 100644 --- a/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts +++ b/src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 87cddb00a8d..ad79eb6a01f 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -5,11 +5,11 @@ import { addDisposableListener, getActiveDocument } from 'vs/base/browser/dom'; import { coalesce } from 'vs/base/common/arrays'; -import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { UriList, VSDataTransfer, createStringDataTransferItem, matchesMimeType } from 'vs/base/common/dataTransfer'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import * as platform from 'vs/base/common/platform'; import { generateUuid } from 'vs/base/common/uuid'; @@ -36,6 +36,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { PostEditWidgetManager } from './postEditWidget'; +import { CancellationError, isCancellationError } from 'vs/base/common/errors'; export const changePasteTypeCommandId = 'editor.changePasteType'; @@ -54,6 +55,12 @@ type PasteEditWithProvider = DocumentPasteEdit & { provider: DocumentPasteEditProvider; }; + +interface DocumentPasteWithProviderEditsSession { + edits: readonly PasteEditWithProvider[]; + dispose(): void; +} + type PastePreference = | HierarchicalKind | { providerId: string }; @@ -299,17 +306,28 @@ export class CopyPasteController extends Disposable implements IEditorContributi } private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, clipboardEvent: ClipboardEvent): void { - const p = createCancelablePromise(async (token) => { + const editor = this._editor; + if (!editor.hasModel()) { + return; + } + + const editorStateCts = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined); + + const p = createCancelablePromise(async (pToken) => { const editor = this._editor; if (!editor.hasModel()) { return; } const model = editor.getModel(); - const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token); + const disposables = new DisposableStore(); + const cts = disposables.add(new CancellationTokenSource(pToken)); + disposables.add(editorStateCts.token.onCancellationRequested(() => cts.cancel())); + + const token = cts.token; try { - await this.mergeInDataFromCopy(dataTransfer, metadata, tokenSource.token); - if (tokenSource.token.isCancellationRequested) { + await this.mergeInDataFromCopy(dataTransfer, metadata, token); + if (token.isCancellationRequested) { return; } @@ -317,43 +335,75 @@ export class CopyPasteController extends Disposable implements IEditorContributi if (!supportedProviders.length || (supportedProviders.length === 1 && supportedProviders[0] instanceof DefaultTextPasteOrDropEditProvider) // Only our default text provider is active ) { - return this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); + return this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent); } const context: DocumentPasteContext = { triggerKind: DocumentPasteTriggerKind.Automatic, }; - const providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); - if (tokenSource.token.isCancellationRequested) { + const editSession = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, token); + disposables.add(editSession); + if (token.isCancellationRequested) { return; } // If the only edit returned is our default text edit, use the default paste handler - if (providerEdits.length === 1 && providerEdits[0].provider instanceof DefaultTextPasteOrDropEditProvider) { - return this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); + if (editSession.edits.length === 1 && editSession.edits[0].provider instanceof DefaultTextPasteOrDropEditProvider) { + return this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent); } - if (providerEdits.length) { + if (editSession.edits.length) { const canShowWidget = editor.getOption(EditorOption.pasteAs).showPasteSelector === 'afterPaste'; - return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: 0, allEdits: providerEdits }, canShowWidget, async (edit, token) => { - const resolved = await edit.provider.resolveDocumentPasteEdit?.(edit, token); - if (resolved) { - edit.additionalEdit = resolved.additionalEdit; - } - return edit; - }, tokenSource.token); + return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: 0, allEdits: editSession.edits }, canShowWidget, (edit, token) => { + return new Promise((resolve, reject) => { + (async () => { + try { + const resolveP = edit.provider.resolveDocumentPasteEdit?.(edit, token); + const showP = new DeferredPromise(); + const resolved = resolveP && await this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('resolveProcess', "Resolving paste edit. Click to cancel"), Promise.race([showP.p, resolveP]), { + cancel: () => { + showP.cancel(); + return reject(new CancellationError()); + } + }, 0); + if (resolved) { + edit.additionalEdit = resolved.additionalEdit; + } + return resolve(edit); + } catch (err) { + return reject(err); + } + })(); + }); + }, token); } - await this.applyDefaultPasteHandler(dataTransfer, metadata, tokenSource.token, clipboardEvent); + await this.applyDefaultPasteHandler(dataTransfer, metadata, token, clipboardEvent); } finally { - tokenSource.dispose(); + disposables.dispose(); if (this._currentPasteOperation === p) { this._currentPasteOperation = undefined; } } }); - this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel"), p); + this._pasteProgressManager.showWhile(selections[0].getEndPosition(), localize('pasteIntoEditorProgress', "Running paste handlers. Click to cancel and do basic paste"), p, { + cancel: async () => { + try { + p.cancel(); + + if (editorStateCts.token.isCancellationRequested) { + return; + } + + await this.applyDefaultPasteHandler(dataTransfer, metadata, editorStateCts.token, clipboardEvent); + } finally { + editorStateCts.dispose(); + } + } + }).then(() => { + editorStateCts.dispose(); + }); this._currentPasteOperation = p; } @@ -365,7 +415,8 @@ export class CopyPasteController extends Disposable implements IEditorContributi } const model = editor.getModel(); - const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token); + const disposables = new DisposableStore(); + const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection, undefined, token)); try { await this.mergeInDataFromCopy(dataTransfer, metadata, tokenSource.token); if (tokenSource.token.isCancellationRequested) { @@ -383,23 +434,26 @@ export class CopyPasteController extends Disposable implements IEditorContributi triggerKind: DocumentPasteTriggerKind.PasteAs, only: preference && preference instanceof HierarchicalKind ? preference : undefined, }; - let providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token); + let editSession = disposables.add(await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, context, tokenSource.token)); if (tokenSource.token.isCancellationRequested) { return; } // Filter out any edits that don't match the requested kind if (preference) { - providerEdits = providerEdits.filter(edit => { - if (preference instanceof HierarchicalKind) { - return preference.contains(edit.kind); - } else { - return preference.providerId === edit.provider.id; - } - }); + editSession = { + edits: editSession.edits.filter(edit => { + if (preference instanceof HierarchicalKind) { + return preference.contains(edit.kind); + } else { + return preference.providerId === edit.provider.id; + } + }), + dispose: editSession.dispose + }; } - if (!providerEdits.length) { + if (!editSession.edits.length) { if (context.only) { this.showPasteAsNoEditMessage(selections, context.only); } @@ -408,10 +462,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi let pickedEdit: DocumentPasteEdit | undefined; if (preference) { - pickedEdit = providerEdits.at(0); + pickedEdit = editSession.edits.at(0); } else { const selected = await this._quickInputService.pick( - providerEdits.map((edit): IQuickPickItem & { edit: DocumentPasteEdit } => ({ + editSession.edits.map((edit): IQuickPickItem & { edit: DocumentPasteEdit } => ({ label: edit.title, description: edit.kind?.value, edit, @@ -428,7 +482,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi const combinedWorkspaceEdit = createCombinedWorkspaceEdit(model.uri, selections, pickedEdit); await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor }); } finally { - tokenSource.dispose(); + disposables.dispose(); if (this._currentPasteOperation === p) { this._currentPasteOperation = undefined; } @@ -499,23 +553,32 @@ export class CopyPasteController extends Disposable implements IEditorContributi } } - private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise { + private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], context: DocumentPasteContext, token: CancellationToken): Promise { + const disposables = new DisposableStore(); + const results = await raceCancellation( Promise.all(providers.map(async provider => { try { const edits = await provider.provideDocumentPasteEdits?.(model, selections, dataTransfer, context, token); - // TODO: dispose of edits + if (edits) { + disposables.add(edits); + } return edits?.edits?.map(edit => ({ ...edit, provider })); } catch (err) { - console.error(err); + if (!isCancellationError(err)) { + console.error(err); + } + return undefined; } - return undefined; })), token); const edits = coalesce(results ?? []).flat().filter(edit => { return !context.only || context.only.contains(edit.kind); }); - return sortEditsByYieldTo(edits); + return { + edits: sortEditsByYieldTo(edits), + dispose: () => disposables.dispose() + }; } private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken, clipboardEvent: ClipboardEvent) { diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts index 006713618a6..e44d9341c9b 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts @@ -14,7 +14,7 @@ import { relativePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; -import { DocumentDropEdit, DocumentDropEditProvider, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; +import { DocumentDropEditProvider, DocumentDropEditsSession, DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession, DocumentPasteTriggerKind } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; @@ -34,14 +34,20 @@ abstract class SimplePasteAndDropProvider implements DocumentDropEditProvider, D } return { + edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }], dispose() { }, - edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] }; } - async provideDocumentDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const edit = await this.getEdit(dataTransfer, token); - return edit ? [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }] : undefined; + if (!edit) { + return; + } + return { + edits: [{ insertText: edit.insertText, title: edit.title, kind: edit.kind, handledMimeType: edit.handledMimeType, yieldTo: edit.yieldTo }], + dispose() { }, + }; } protected abstract getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise; diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts index 20adc65da11..082b7447881 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts @@ -7,7 +7,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, raceCancellation } from 'vs/base/common/async'; import { VSDataTransfer, matchesMimeType } from 'vs/base/common/dataTransfer'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { toExternalVSDataTransfer } from 'vs/editor/browser/dnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -84,8 +84,9 @@ export class DropIntoEditorController extends Disposable implements IEditorContr editor.setPosition(position); const p = createCancelablePromise(async (token) => { - const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value, undefined, token); + const disposables = new DisposableStore(); + const tokenSource = disposables.add(new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value, undefined, token)); try { const ourDataTransfer = await this.extractDataTransferData(dragEvent); if (ourDataTransfer.size === 0 || tokenSource.token.isCancellationRequested) { @@ -107,34 +108,39 @@ export class DropIntoEditorController extends Disposable implements IEditorContr return provider.dropMimeTypes.some(mime => ourDataTransfer.matches(mime)); }); - const edits = await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource); + const editSession = disposables.add(await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource)); if (tokenSource.token.isCancellationRequested) { return; } - if (edits.length) { - const activeEditIndex = this.getInitialActiveEditIndex(model, edits); + if (editSession.edits.length) { + const activeEditIndex = this.getInitialActiveEditIndex(model, editSession.edits); const canShowWidget = editor.getOption(EditorOption.dropIntoEditor).showDropSelector === 'afterDrop'; // Pass in the parent token here as it tracks cancelling the entire drop operation - await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: edits }, canShowWidget, async edit => edit, token); + await this._postDropWidgetManager.applyEditAndShowIfNeeded([Range.fromPositions(position)], { activeEditIndex, allEdits: editSession.edits }, canShowWidget, async edit => edit, token); } } finally { - tokenSource.dispose(); + disposables.dispose(); if (this._currentOperation === p) { this._currentOperation = undefined; } } }); - this._dropProgressManager.showWhile(position, localize('dropIntoEditorProgress', "Running drop handlers. Click to cancel"), p); + this._dropProgressManager.showWhile(position, localize('dropIntoEditorProgress', "Running drop handlers. Click to cancel"), p, { cancel: () => p.cancel() }); this._currentOperation = p; } private async getDropEdits(providers: readonly DocumentDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, tokenSource: EditorStateCancellationTokenSource) { + const disposables = new DisposableStore(); + const results = await raceCancellation(Promise.all(providers.map(async provider => { try { const edits = await provider.provideDocumentDropEdits(model, position, dataTransfer, tokenSource.token); - return edits?.map(edit => ({ ...edit, providerId: provider.id })); + if (edits) { + disposables.add(edits); + } + return edits?.edits.map(edit => ({ ...edit, providerId: provider.id })); } catch (err) { console.error(err); } @@ -142,7 +148,10 @@ export class DropIntoEditorController extends Disposable implements IEditorContr })), tokenSource.token); const edits = coalesce(results ?? []).flat(); - return sortEditsByYieldTo(edits); + return { + edits: sortEditsByYieldTo(edits), + dispose: () => disposables.dispose() + }; } private getInitialActiveEditIndex(model: ITextModel, edits: ReadonlyArray) { diff --git a/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts b/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts index ee0b8e56b61..ec61f04a463 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/test/browser/editSort.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DocumentDropEdit } from 'vs/editor/common/languages'; diff --git a/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts b/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts index cc2a8d0f93e..239da13d225 100644 --- a/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts +++ b/src/vs/editor/contrib/editorState/test/browser/editorState.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/find/browser/findDecorations.ts b/src/vs/editor/contrib/find/browser/findDecorations.ts index 62bfb58fb9c..e5e9018464a 100644 --- a/src/vs/editor/contrib/find/browser/findDecorations.ts +++ b/src/vs/editor/contrib/find/browser/findDecorations.ts @@ -288,6 +288,7 @@ export class FindDecorations implements IDisposable { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, zIndex: 13, className: 'currentFindMatch', + inlineClassName: 'currentFindMatchInline', showIfCollapsed: true, overviewRuler: { color: themeColorFromId(overviewRulerFindMatchForeground), @@ -304,6 +305,7 @@ export class FindDecorations implements IDisposable { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, zIndex: 10, className: 'findMatch', + inlineClassName: 'findMatchInline', showIfCollapsed: true, overviewRuler: { color: themeColorFromId(overviewRulerFindMatchForeground), diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index d3949c02e72..9645d4b54ba 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -35,7 +35,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { asCssVariable, contrastBorder, editorFindMatchHighlightBorder, editorFindRangeHighlightBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; +import { asCssVariable, contrastBorder, editorFindMatchForeground, editorFindMatchHighlightBorder, editorFindMatchHighlightForeground, editorFindRangeHighlightBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerIcon, widgetClose } from 'vs/platform/theme/common/iconRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -47,10 +47,10 @@ import { createInstantHoverDelegate, getDefaultHoverDelegate } from 'vs/base/bro import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IHoverService } from 'vs/platform/hover/browser/hover'; -const findSelectionIcon = registerIcon('find-selection', Codicon.selection, nls.localize('findSelectionIcon', 'Icon for \'Find in Selection\' in the editor find widget.')); const findCollapsedIcon = registerIcon('find-collapsed', Codicon.chevronRight, nls.localize('findCollapsedIcon', 'Icon to indicate that the editor find widget is collapsed.')); const findExpandedIcon = registerIcon('find-expanded', Codicon.chevronDown, nls.localize('findExpandedIcon', 'Icon to indicate that the editor find widget is expanded.')); +export const findSelectionIcon = registerIcon('find-selection', Codicon.selection, nls.localize('findSelectionIcon', 'Icon for \'Find in Selection\' in the editor find widget.')); export const findReplaceIcon = registerIcon('find-replace', Codicon.replace, nls.localize('findReplaceIcon', 'Icon for \'Replace\' in the editor find widget.')); export const findReplaceAllIcon = registerIcon('find-replace-all', Codicon.replaceAll, nls.localize('findReplaceAllIcon', 'Icon for \'Replace All\' in the editor find widget.')); export const findPreviousMatchIcon = registerIcon('find-previous-match', Codicon.arrowUp, nls.localize('findPreviousMatchIcon', 'Icon for \'Find Previous\' in the editor find widget.')); @@ -410,9 +410,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL } // remove previous content - if (this._matchesCount.firstChild) { - this._matchesCount.removeChild(this._matchesCount.firstChild); - } + this._matchesCount.firstChild?.remove(); let label: string; if (this._state.matchesCount > 0) { @@ -1345,7 +1343,7 @@ export class SimpleButton extends Widget { this._domNode.className = className; this._domNode.setAttribute('role', 'button'); this._domNode.setAttribute('aria-label', this._opts.label); - this._register(hoverService.setupUpdatableHover(opts.hoverDelegate ?? getDefaultHoverDelegate('element'), this._domNode, this._opts.label)); + this._register(hoverService.setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('element'), this._domNode, this._opts.label)); this.onclick(this._domNode, (e) => { this._opts.onTrigger(); @@ -1409,4 +1407,12 @@ registerThemingParticipant((theme, collector) => { if (hcBorder) { collector.addRule(`.monaco-editor .find-widget { border: 1px solid ${hcBorder}; }`); } + const findMatchForeground = theme.getColor(editorFindMatchForeground); + if (findMatchForeground) { + collector.addRule(`.monaco-editor .findMatchInline { color: ${findMatchForeground}; }`); + } + const findMatchHighlightForeground = theme.getColor(editorFindMatchHighlightForeground); + if (findMatchHighlightForeground) { + collector.addRule(`.monaco-editor .currentFindMatchInline { color: ${findMatchHighlightForeground}; }`); + } }); diff --git a/src/vs/editor/contrib/find/test/browser/find.test.ts b/src/vs/editor/contrib/find/test/browser/find.test.ts index 466c39baf1e..580ea739b76 100644 --- a/src/vs/editor/contrib/find/test/browser/find.test.ts +++ b/src/vs/editor/contrib/find/test/browser/find.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/contrib/find/test/browser/findController.test.ts b/src/vs/editor/contrib/find/test/browser/findController.test.ts index 9bdb1cb7786..49fef95baf8 100644 --- a/src/vs/editor/contrib/find/test/browser/findController.test.ts +++ b/src/vs/editor/contrib/find/test/browser/findController.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Delayer } from 'vs/base/common/async'; import * as platform from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/contrib/find/test/browser/findModel.test.ts b/src/vs/editor/contrib/find/test/browser/findModel.test.ts index 7d1ae5f5bd5..8cc753388b9 100644 --- a/src/vs/editor/contrib/find/test/browser/findModel.test.ts +++ b/src/vs/editor/contrib/find/test/browser/findModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreNavigationCommands } from 'vs/editor/browser/coreCommands'; diff --git a/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts b/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts index cc9e76c93b5..1f534bbdae3 100644 --- a/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts +++ b/src/vs/editor/contrib/find/test/browser/replacePattern.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseReplaceString, ReplacePattern, ReplacePiece } from 'vs/editor/contrib/find/browser/replacePattern'; diff --git a/src/vs/editor/contrib/folding/browser/folding.css b/src/vs/editor/contrib/folding/browser/folding.css index f973d5f7a30..5f7ab05db78 100644 --- a/src/vs/editor/contrib/folding/browser/folding.css +++ b/src/vs/editor/contrib/folding/browser/folding.css @@ -31,7 +31,7 @@ } .monaco-editor .inline-folded:after { - color: grey; + color: var(--vscode-editor-foldPlaceholderForeground); margin: 0.1em 0.2em 0 0.2em; content: "\22EF"; /* ellipses unicode character */ display: inline; @@ -49,4 +49,3 @@ .monaco-editor .cldr.codicon.codicon-folding-manual-collapsed { color: var(--vscode-editorGutter-foldingControlForeground) !important; } - diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index 993c8cf0827..25988ac7370 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -809,6 +809,30 @@ class FoldRecursivelyAction extends FoldingAction { } } + +class ToggleFoldRecursivelyAction extends FoldingAction { + + constructor() { + super({ + id: 'editor.toggleFoldRecursively', + label: nls.localize('toggleFoldRecursivelyAction.label', "Toggle Fold Recursively"), + alias: 'Toggle Fold Recursively', + precondition: CONTEXT_FOLDING_ENABLED, + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL), + weight: KeybindingWeight.EditorContrib + } + }); + } + + invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void { + const selectedLines = this.getSelectedLines(editor); + toggleCollapseState(foldingModel, Number.MAX_VALUE, selectedLines); + } +} + + class FoldAllBlockCommentsAction extends FoldingAction { constructor() { @@ -1189,6 +1213,7 @@ registerEditorAction(UnfoldAction); registerEditorAction(UnFoldRecursivelyAction); registerEditorAction(FoldAction); registerEditorAction(FoldRecursivelyAction); +registerEditorAction(ToggleFoldRecursivelyAction); registerEditorAction(FoldAllAction); registerEditorAction(UnfoldAllAction); registerEditorAction(FoldAllBlockCommentsAction); diff --git a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts index 03a9b4a402c..2350f9aad34 100644 --- a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts @@ -15,7 +15,8 @@ import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; const foldBackground = registerColor('editor.foldBackground', { light: transparent(editorSelectionBackground, 0.3), dark: transparent(editorSelectionBackground, 0.3), hcDark: null, hcLight: null }, localize('foldBackgroundBackground', "Background color behind folded ranges. The color must not be opaque so as not to hide underlying decorations."), true); -registerColor('editorGutter.foldingControlForeground', { dark: iconForeground, light: iconForeground, hcDark: iconForeground, hcLight: iconForeground }, localize('editorGutter.foldingControlForeground', 'Color of the folding control in the editor gutter.')); +registerColor('editor.foldPlaceholderForeground', { light: '#808080', dark: '#808080', hcDark: null, hcLight: null }, localize('collapsedTextColor', "Color of the collapsed text after the first line of a folded range.")); +registerColor('editorGutter.foldingControlForeground', iconForeground, localize('editorGutter.foldingControlForeground', 'Color of the folding control in the editor gutter.')); export const foldingExpandedIcon = registerIcon('folding-expanded', Codicon.chevronDown, localize('foldingExpandedIcon', 'Icon for expanded ranges in the editor glyph margin.')); export const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.chevronRight, localize('foldingCollapsedIcon', 'Icon for collapsed ranges in the editor glyph margin.')); diff --git a/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts b/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts index 3cef0c2a56e..1ea615a5bd9 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts index 912cf515d1a..a1a6dbe1dc1 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { FoldingMarkers } from 'vs/editor/common/languages/languageConfiguration'; import { MAX_FOLDING_REGIONS, FoldRange, FoldingRegions, FoldSource } from 'vs/editor/contrib/folding/browser/foldingRanges'; diff --git a/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts b/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts index 71281efa963..6d57dec4e78 100644 --- a/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/hiddenRangeModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IRange } from 'vs/editor/common/core/range'; import { FoldingModel } from 'vs/editor/contrib/folding/browser/foldingModel'; import { HiddenRangeModel } from 'vs/editor/contrib/folding/browser/hiddenRangeModel'; diff --git a/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts b/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts index afee83dff29..25db0e6d15d 100644 --- a/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { computeRanges } from 'vs/editor/contrib/folding/browser/indentRangeProvider'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; diff --git a/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts b/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts index 0a1c3220e64..837dd6c0bc4 100644 --- a/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/indentRangeProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { FoldingMarkers } from 'vs/editor/common/languages/languageConfiguration'; import { computeRanges } from 'vs/editor/contrib/folding/browser/indentRangeProvider'; diff --git a/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts b/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts index 3551f446606..3b2bdb0648e 100644 --- a/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModel } from 'vs/editor/common/model'; import { FoldingContext, FoldingRange, FoldingRangeProvider, ProviderResult } from 'vs/editor/common/languages'; diff --git a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts index 24ffac212bd..cf0e5c7d6d0 100644 --- a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts @@ -308,10 +308,9 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._disposables.add(this._actionbarWidget!.actionRunner.onWillRun(e => this.editor.focus())); const actions: IAction[] = []; - const menu = this._menuService.createMenu(MarkerNavigationWidget.TitleMenu, this._contextKeyService); - createAndFillInActionBarActions(menu, undefined, actions); + const menu = this._menuService.getMenuActions(MarkerNavigationWidget.TitleMenu, this._contextKeyService); + createAndFillInActionBarActions(menu, actions); this._actionbarWidget!.push(actions, { label: false, icon: true, index: 0 }); - menu.dispose(); } protected override _fillTitleIcon(container: HTMLElement): void { @@ -410,4 +409,4 @@ const editorMarkerNavigationWarningHeader = registerColor('editorMarkerNavigatio const editorMarkerNavigationInfo = registerColor('editorMarkerNavigationInfo.background', { dark: infoDefault, light: infoDefault, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('editorMarkerNavigationInfo', 'Editor marker navigation widget info color.')); const editorMarkerNavigationInfoHeader = registerColor('editorMarkerNavigationInfo.headerBackground', { dark: transparent(editorMarkerNavigationInfo, .1), light: transparent(editorMarkerNavigationInfo, .1), hcDark: null, hcLight: null }, nls.localize('editorMarkerNavigationInfoHeaderBackground', 'Editor marker navigation widget info heading background.')); -const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', { dark: editorBackground, light: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.')); +const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', editorBackground, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.')); diff --git a/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts b/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts index 8ebd1d2ce26..738af806684 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/goToSymbol.ts @@ -22,7 +22,7 @@ function shouldIncludeLocationLink(sourceModel: ITextModel, loc: LocationLink): } // Otherwise filter out locations from internal schemes - if (matchesSomeScheme(loc.uri, Schemas.walkThroughSnippet, Schemas.vscodeChatCodeBlock, Schemas.vscodeChatCodeCompareBlock)) { + if (matchesSomeScheme(loc.uri, Schemas.walkThroughSnippet, Schemas.vscodeChatCodeBlock, Schemas.vscodeChatCodeCompareBlock, Schemas.vscodeCopilotBackingChatCodeBlock)) { return false; } diff --git a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts index b80e75d47ec..0941671756d 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts @@ -390,7 +390,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { this._onDidSelectReference.fire({ element, kind, source: 'tree' }); } }; - this._tree.onDidOpen(e => { + this._disposables.add(this._tree.onDidOpen(e => { if (e.sideBySide) { onEvent(e.element, 'side'); } else if (e.editorOptions.pinned) { @@ -398,7 +398,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { } else { onEvent(e.element, 'show'); } - }); + })); dom.hide(this._treeContainer); } diff --git a/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts b/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts index d1195449779..a547f9450e1 100644 --- a/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts +++ b/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/contrib/hover/browser/contentHoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverController.ts index 20c441162b5..d79984f24c1 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverController.ts @@ -5,59 +5,62 @@ import * as dom from 'vs/base/browser/dom'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { TokenizationRegistry } from 'vs/editor/common/languages'; import { HoverOperation, HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; -import { HoverAnchor, HoverParticipantRegistry, HoverRangeAnchor, IEditorHoverColorPickerWidget, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IHoverWidget } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverParticipantRegistry, HoverRangeAnchor, IEditorHoverContext, IEditorHoverParticipant, IHoverPart, IHoverWidget } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; -import { InlayHintsHover } from 'vs/editor/contrib/inlayHints/browser/inlayHintsHover'; import { HoverVerbosityAction } from 'vs/editor/common/standalone/standaloneEnums'; import { ContentHoverWidget } from 'vs/editor/contrib/hover/browser/contentHoverWidget'; import { ContentHoverComputer } from 'vs/editor/contrib/hover/browser/contentHoverComputer'; -import { ContentHoverVisibleData, HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; -import { EditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/contentHoverStatusBar'; +import { HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; +import { Emitter } from 'vs/base/common/event'; +import { RenderedContentHover } from 'vs/editor/contrib/hover/browser/contentHoverRendered'; export class ContentHoverController extends Disposable implements IHoverWidget { private _currentResult: HoverResult | null = null; + private _renderedContentHover: RenderedContentHover | undefined; private readonly _computer: ContentHoverComputer; - private readonly _widget: ContentHoverWidget; + private readonly _contentHoverWidget: ContentHoverWidget; private readonly _participants: IEditorHoverParticipant[]; - // TODO@aiday-mar make array of participants, dispatch between them - private readonly _markdownHoverParticipant: MarkdownHoverParticipant | undefined; private readonly _hoverOperation: HoverOperation; + private readonly _onContentsChanged = this._register(new Emitter()); + public readonly onContentsChanged = this._onContentsChanged.event; + constructor( private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); - - this._widget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor)); - - // Instantiate participants and sort them by `hoverOrdinal` which is relevant for rendering order. - this._participants = []; - for (const participant of HoverParticipantRegistry.getAll()) { - const participantInstance = this._instantiationService.createInstance(participant, this._editor); - if (participantInstance instanceof MarkdownHoverParticipant && !(participantInstance instanceof InlayHintsHover)) { - this._markdownHoverParticipant = participantInstance; - } - this._participants.push(participantInstance); - } - this._participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal); - + this._contentHoverWidget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor)); + this._participants = this._initializeHoverParticipants(); this._computer = new ContentHoverComputer(this._editor, this._participants); this._hoverOperation = this._register(new HoverOperation(this._editor, this._computer)); + this._registerListeners(); + } + private _initializeHoverParticipants(): IEditorHoverParticipant[] { + const participants: IEditorHoverParticipant[] = []; + for (const participant of HoverParticipantRegistry.getAll()) { + const participantInstance = this._instantiationService.createInstance(participant, this._editor); + participants.push(participantInstance); + } + participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal); + this._register(this._contentHoverWidget.onDidResize(() => { + this._participants.forEach(participant => participant.handleResize?.()); + })); + return participants; + } + + private _registerListeners(): void { this._register(this._hoverOperation.onResult((result) => { if (!this._computer.anchor) { // invalid state, ignore result @@ -66,13 +69,13 @@ export class ContentHoverController extends Disposable implements IHoverWidget { const messages = (result.hasLoadingMessage ? this._addLoadingMessage(result.value) : result.value); this._withResult(new HoverResult(this._computer.anchor, messages, result.isComplete)); })); - this._register(dom.addStandardDisposableListener(this._widget.getDomNode(), 'keydown', (e) => { + this._register(dom.addStandardDisposableListener(this._contentHoverWidget.getDomNode(), 'keydown', (e) => { if (e.equals(KeyCode.Escape)) { this.hide(); } })); this._register(TokenizationRegistry.onDidChange(() => { - if (this._widget.position && this._currentResult) { + if (this._contentHoverWidget.position && this._currentResult) { this._setCurrentResult(this._currentResult); // render again } })); @@ -88,61 +91,52 @@ export class ContentHoverController extends Disposable implements IHoverWidget { focus: boolean, mouseEvent: IEditorMouseEvent | null ): boolean { - - if (!this._widget.position || !this._currentResult) { - // The hover is not visible + const contentHoverIsVisible = this._contentHoverWidget.position && this._currentResult; + if (!contentHoverIsVisible) { if (anchor) { this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); return true; } return false; } - - // The hover is currently visible const isHoverSticky = this._editor.getOption(EditorOption.hover).sticky; - const isGettingCloser = ( - isHoverSticky - && mouseEvent - && this._widget.isMouseGettingCloser(mouseEvent.event.posx, mouseEvent.event.posy) - ); - - if (isGettingCloser) { - // The mouse is getting closer to the hover, so we will keep the hover untouched - // But we will kick off a hover update at the new anchor, insisting on keeping the hover visible. + const isMouseGettingCloser = mouseEvent && this._contentHoverWidget.isMouseGettingCloser(mouseEvent.event.posx, mouseEvent.event.posy); + const isHoverStickyAndIsMouseGettingCloser = isHoverSticky && isMouseGettingCloser; + // The mouse is getting closer to the hover, so we will keep the hover untouched + // But we will kick off a hover update at the new anchor, insisting on keeping the hover visible. + if (isHoverStickyAndIsMouseGettingCloser) { if (anchor) { this._startHoverOperationIfNecessary(anchor, mode, source, focus, true); } return true; } - + // If mouse is not getting closer and anchor not defined, hide the hover if (!anchor) { this._setCurrentResult(null); return false; } - - if (anchor && this._currentResult.anchor.equals(anchor)) { - // The widget is currently showing results for the exact same anchor, so no update is needed + // If mouse if not getting closer and anchor is defined, and the new anchor is the same as the previous anchor + const currentAnchorEqualsPreviousAnchor = this._currentResult!.anchor.equals(anchor); + if (currentAnchorEqualsPreviousAnchor) { return true; } - - if (!anchor.canAdoptVisibleHover(this._currentResult.anchor, this._widget.position)) { - // The new anchor is not compatible with the previous anchor + // If mouse if not getting closer and anchor is defined, and the new anchor is not compatible with the previous anchor + const currentAnchorCompatibleWithPreviousAnchor = anchor.canAdoptVisibleHover(this._currentResult!.anchor, this._contentHoverWidget.position); + if (!currentAnchorCompatibleWithPreviousAnchor) { this._setCurrentResult(null); this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); return true; } - // We aren't getting any closer to the hover, so we will filter existing results // and keep those which also apply to the new anchor. - this._setCurrentResult(this._currentResult.filter(anchor)); + this._setCurrentResult(this._currentResult!.filter(anchor)); this._startHoverOperationIfNecessary(anchor, mode, source, focus, false); return true; } private _startHoverOperationIfNecessary(anchor: HoverAnchor, mode: HoverStartMode, source: HoverStartSource, focus: boolean, insistOnKeepingHoverVisible: boolean): void { - - if (this._computer.anchor && this._computer.anchor.equals(anchor)) { - // We have to start a hover operation at the exact same anchor as before, so no work is needed + const currentAnchorEqualToPreviousHover = this._computer.anchor && this._computer.anchor.equals(anchor); + if (currentAnchorEqualToPreviousHover) { return; } this._hoverOperation.cancel(); @@ -154,253 +148,213 @@ export class ContentHoverController extends Disposable implements IHoverWidget { } private _setCurrentResult(hoverResult: HoverResult | null): void { - - if (this._currentResult === hoverResult) { - // avoid updating the DOM to avoid resetting the user selection + let currentHoverResult = hoverResult; + const currentResultEqualToPreviousResult = this._currentResult === currentHoverResult; + if (currentResultEqualToPreviousResult) { return; } - if (hoverResult && hoverResult.messages.length === 0) { - hoverResult = null; + const currentHoverResultIsEmpty = currentHoverResult && currentHoverResult.hoverParts.length === 0; + if (currentHoverResultIsEmpty) { + currentHoverResult = null; } - this._currentResult = hoverResult; + this._currentResult = currentHoverResult; if (this._currentResult) { - this._renderMessages(this._currentResult.anchor, this._currentResult.messages); + this._showHover(this._currentResult); } else { - this._widget.hide(); + this._hideHover(); } } private _addLoadingMessage(result: IHoverPart[]): IHoverPart[] { - if (this._computer.anchor) { - for (const participant of this._participants) { - if (participant.createLoadingMessage) { - const loadingMessage = participant.createLoadingMessage(this._computer.anchor); - if (loadingMessage) { - return result.slice(0).concat([loadingMessage]); - } - } + if (!this._computer.anchor) { + return result; + } + for (const participant of this._participants) { + if (!participant.createLoadingMessage) { + continue; } + const loadingMessage = participant.createLoadingMessage(this._computer.anchor); + if (!loadingMessage) { + continue; + } + return result.slice(0).concat([loadingMessage]); } return result; } private _withResult(hoverResult: HoverResult): void { - if (this._widget.position && this._currentResult && this._currentResult.isComplete) { - // The hover is visible with a previous complete result. - - if (!hoverResult.isComplete) { - // Instead of rendering the new partial result, we wait for the result to be complete. - return; - } - - if (this._computer.insistOnKeepingHoverVisible && hoverResult.messages.length === 0) { - // The hover would now hide normally, so we'll keep the previous messages - return; - } + const previousHoverIsVisibleWithCompleteResult = this._contentHoverWidget.position && this._currentResult && this._currentResult.isComplete; + if (!previousHoverIsVisibleWithCompleteResult) { + this._setCurrentResult(hoverResult); + } + // The hover is visible with a previous complete result. + const isCurrentHoverResultComplete = hoverResult.isComplete; + if (!isCurrentHoverResultComplete) { + // Instead of rendering the new partial result, we wait for the result to be complete. + return; + } + const currentHoverResultIsEmpty = hoverResult.hoverParts.length === 0; + const insistOnKeepingPreviousHoverVisible = this._computer.insistOnKeepingHoverVisible; + const shouldKeepPreviousHoverVisible = currentHoverResultIsEmpty && insistOnKeepingPreviousHoverVisible; + if (shouldKeepPreviousHoverVisible) { + // The hover would now hide normally, so we'll keep the previous messages + return; } - this._setCurrentResult(hoverResult); } - private _renderMessages(anchor: HoverAnchor, messages: IHoverPart[]): void { - const { showAtPosition, showAtSecondaryPosition, highlightRange } = ContentHoverController.computeHoverRanges(this._editor, anchor.range, messages); - - const disposables = new DisposableStore(); - const statusBar = disposables.add(new EditorHoverStatusBar(this._keybindingService)); - const fragment = document.createDocumentFragment(); - - let colorPicker: IEditorHoverColorPickerWidget | null = null; - const context: IEditorHoverRenderContext = { - fragment, - statusBar, - setColorPicker: (widget) => colorPicker = widget, - onContentsChanged: () => this._widget.onContentsChanged(), - setMinimumDimensions: (dimensions: dom.Dimension) => this._widget.setMinimumDimensions(dimensions), - hide: () => this.hide() - }; - - for (const participant of this._participants) { - const hoverParts = messages.filter(msg => msg.owner === participant); - if (hoverParts.length > 0) { - disposables.add(participant.renderHoverParts(context, hoverParts)); - } - } - - const isBeforeContent = messages.some(m => m.isBeforeContent); - - if (statusBar.hasContent) { - fragment.appendChild(statusBar.hoverElement); - } - - if (fragment.hasChildNodes()) { - if (highlightRange) { - const highlightDecoration = this._editor.createDecorationsCollection(); - highlightDecoration.set([{ - range: highlightRange, - options: ContentHoverController._DECORATION_OPTIONS - }]); - disposables.add(toDisposable(() => { - highlightDecoration.clear(); - })); - } - - this._widget.showAt(fragment, new ContentHoverVisibleData( - anchor.initialMousePosX, - anchor.initialMousePosY, - colorPicker, - showAtPosition, - showAtSecondaryPosition, - this._editor.getOption(EditorOption.hover).above, - this._computer.shouldFocus, - this._computer.source, - isBeforeContent, - disposables - )); + private _showHover(hoverResult: HoverResult): void { + const context = this._getHoverContext(); + this._renderedContentHover = new RenderedContentHover(this._editor, hoverResult, this._participants, this._computer, context, this._keybindingService); + if (this._renderedContentHover.domNodeHasChildren) { + this._contentHoverWidget.show(this._renderedContentHover); } else { - disposables.dispose(); + this._renderedContentHover.dispose(); } } - private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ - description: 'content-hover-highlight', - className: 'hoverHighlight' - }); + private _hideHover(): void { + this._contentHoverWidget.hide(); + } - public static computeHoverRanges(editor: ICodeEditor, anchorRange: Range, messages: IHoverPart[]) { - - let startColumnBoundary = 1; - if (editor.hasModel()) { - // Ensure the range is on the current view line - const viewModel = editor._getViewModel(); - const coordinatesConverter = viewModel.coordinatesConverter; - const anchorViewRange = coordinatesConverter.convertModelRangeToViewRange(anchorRange); - const anchorViewRangeStart = new Position(anchorViewRange.startLineNumber, viewModel.getLineMinColumn(anchorViewRange.startLineNumber)); - startColumnBoundary = coordinatesConverter.convertViewPositionToModelPosition(anchorViewRangeStart).column; - } - - // The anchor range is always on a single line - const anchorLineNumber = anchorRange.startLineNumber; - let renderStartColumn = anchorRange.startColumn; - let highlightRange = messages[0].range; - let forceShowAtRange = null; - - for (const msg of messages) { - highlightRange = Range.plusRange(highlightRange, msg.range); - if (msg.range.startLineNumber === anchorLineNumber && msg.range.endLineNumber === anchorLineNumber) { - // this message has a range that is completely sitting on the line of the anchor - renderStartColumn = Math.max(Math.min(renderStartColumn, msg.range.startColumn), startColumnBoundary); - } - if (msg.forceShowAtRange) { - forceShowAtRange = msg.range; - } - } - - const showAtPosition = forceShowAtRange ? forceShowAtRange.getStartPosition() : new Position(anchorLineNumber, anchorRange.startColumn); - const showAtSecondaryPosition = forceShowAtRange ? forceShowAtRange.getStartPosition() : new Position(anchorLineNumber, renderStartColumn); - - return { - showAtPosition, - showAtSecondaryPosition, - highlightRange + private _getHoverContext(): IEditorHoverContext { + const hide = () => { + this.hide(); }; + const onContentsChanged = () => { + this._onContentsChanged.fire(); + this._contentHoverWidget.onContentsChanged(); + }; + const setMinimumDimensions = (dimensions: dom.Dimension) => { + this._contentHoverWidget.setMinimumDimensions(dimensions); + }; + return { hide, onContentsChanged, setMinimumDimensions }; } + public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean { - - if (this._widget.isResizing) { + const isContentWidgetResizing = this._contentHoverWidget.isResizing; + if (isContentWidgetResizing) { return true; } - - const anchorCandidates: HoverAnchor[] = []; - for (const participant of this._participants) { - if (participant.suggestHoverAnchor) { - const anchor = participant.suggestHoverAnchor(mouseEvent); - if (anchor) { - anchorCandidates.push(anchor); - } - } - } - - const target = mouseEvent.target; - - if (target.type === MouseTargetType.CONTENT_TEXT) { - anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); - } - - if (target.type === MouseTargetType.CONTENT_EMPTY) { - const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2; - if ( - !target.detail.isAfterLines - && typeof target.detail.horizontalDistanceToText === 'number' - && target.detail.horizontalDistanceToText < epsilon - ) { - // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough - anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); - } - } - - if (anchorCandidates.length === 0) { + const anchorCandidates: HoverAnchor[] = this._findHoverAnchorCandidates(mouseEvent); + const anchorCandidatesExist = anchorCandidates.length > 0; + if (!anchorCandidatesExist) { return this._startShowingOrUpdateHover(null, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); } + const anchor = anchorCandidates[0]; + return this._startShowingOrUpdateHover(anchor, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); + } + private _findHoverAnchorCandidates(mouseEvent: IEditorMouseEvent): HoverAnchor[] { + const anchorCandidates: HoverAnchor[] = []; + for (const participant of this._participants) { + if (!participant.suggestHoverAnchor) { + continue; + } + const anchor = participant.suggestHoverAnchor(mouseEvent); + if (!anchor) { + continue; + } + anchorCandidates.push(anchor); + } + const target = mouseEvent.target; + switch (target.type) { + case MouseTargetType.CONTENT_TEXT: { + anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); + break; + } + case MouseTargetType.CONTENT_EMPTY: { + const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2; + // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough + const mouseIsWithinLinesAndCloseToHover = !target.detail.isAfterLines + && typeof target.detail.horizontalDistanceToText === 'number' + && target.detail.horizontalDistanceToText < epsilon; + if (!mouseIsWithinLinesAndCloseToHover) { + break; + } + anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); + break; + } + } anchorCandidates.sort((a, b) => b.priority - a.priority); - return this._startShowingOrUpdateHover(anchorCandidates[0], HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); + return anchorCandidates; } public startShowingAtRange(range: Range, mode: HoverStartMode, source: HoverStartSource, focus: boolean): void { this._startShowingOrUpdateHover(new HoverRangeAnchor(0, range, undefined, undefined), mode, source, focus, null); } - public async updateFocusedMarkdownHoverVerbosityLevel(action: HoverVerbosityAction): Promise { - this._markdownHoverParticipant?.updateFocusedMarkdownHoverPartVerbosityLevel(action); - } - public getWidgetContent(): string | undefined { - const node = this._widget.getDomNode(); + const node = this._contentHoverWidget.getDomNode(); if (!node.textContent) { return undefined; } return node.textContent; } + public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise { + this._renderedContentHover?.updateHoverVerbosityLevel(action, index, focus); + } + + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._renderedContentHover?.doesHoverAtIndexSupportVerbosityAction(index, action) ?? false; + } + + public getAccessibleWidgetContent(): string | undefined { + return this._renderedContentHover?.getAccessibleWidgetContent(); + } + + public getAccessibleWidgetContentAtIndex(index: number): string | undefined { + return this._renderedContentHover?.getAccessibleWidgetContentAtIndex(index); + } + + public focusedHoverPartIndex(): number { + return this._renderedContentHover?.focusedHoverPartIndex ?? -1; + } + public containsNode(node: Node | null | undefined): boolean { - return (node ? this._widget.getDomNode().contains(node) : false); + return (node ? this._contentHoverWidget.getDomNode().contains(node) : false); } public focus(): void { - this._widget.focus(); + this._contentHoverWidget.focus(); + } + + public focusHoverPartWithIndex(index: number): void { + this._renderedContentHover?.focusHoverPartWithIndex(index); } public scrollUp(): void { - this._widget.scrollUp(); + this._contentHoverWidget.scrollUp(); } public scrollDown(): void { - this._widget.scrollDown(); + this._contentHoverWidget.scrollDown(); } public scrollLeft(): void { - this._widget.scrollLeft(); + this._contentHoverWidget.scrollLeft(); } public scrollRight(): void { - this._widget.scrollRight(); + this._contentHoverWidget.scrollRight(); } public pageUp(): void { - this._widget.pageUp(); + this._contentHoverWidget.pageUp(); } public pageDown(): void { - this._widget.pageDown(); + this._contentHoverWidget.pageDown(); } public goToTop(): void { - this._widget.goToTop(); + this._contentHoverWidget.goToTop(); } public goToBottom(): void { - this._widget.goToBottom(); + this._contentHoverWidget.goToBottom(); } public hide(): void { @@ -410,26 +364,26 @@ export class ContentHoverController extends Disposable implements IHoverWidget { } public get isColorPickerVisible(): boolean { - return this._widget.isColorPickerVisible; + return this._renderedContentHover?.isColorPickerVisible() ?? false; } public get isVisibleFromKeyboard(): boolean { - return this._widget.isVisibleFromKeyboard; + return this._contentHoverWidget.isVisibleFromKeyboard; } public get isVisible(): boolean { - return this._widget.isVisible; + return this._contentHoverWidget.isVisible; } public get isFocused(): boolean { - return this._widget.isFocused; + return this._contentHoverWidget.isFocused; } public get isResizing(): boolean { - return this._widget.isResizing; + return this._contentHoverWidget.isResizing; } public get widget() { - return this._widget; + return this._contentHoverWidget; } } diff --git a/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts new file mode 100644 index 00000000000..9308e6f295d --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/contentHoverRendered.ts @@ -0,0 +1,436 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEditorHoverContext, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ContentHoverComputer } from 'vs/editor/contrib/hover/browser/contentHoverComputer'; +import { EditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/contentHoverStatusBar'; +import { HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; +import * as dom from 'vs/base/browser/dom'; +import { HoverVerbosityAction } from 'vs/editor/common/languages'; +import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; +import { ColorHoverParticipant } from 'vs/editor/contrib/colorPicker/browser/colorHoverParticipant'; +import { localize } from 'vs/nls'; +import { InlayHintsHover } from 'vs/editor/contrib/inlayHints/browser/inlayHintsHover'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { HoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; + +export class RenderedContentHover extends Disposable { + + public closestMouseDistance: number | undefined; + public initialMousePosX: number | undefined; + public initialMousePosY: number | undefined; + + public readonly showAtPosition: Position; + public readonly showAtSecondaryPosition: Position; + public readonly shouldFocus: boolean; + public readonly source: HoverStartSource; + public readonly shouldAppearBeforeContent: boolean; + + private readonly _renderedHoverParts: RenderedContentHoverParts; + + constructor( + editor: ICodeEditor, + hoverResult: HoverResult, + participants: IEditorHoverParticipant[], + computer: ContentHoverComputer, + context: IEditorHoverContext, + keybindingService: IKeybindingService + ) { + super(); + const anchor = hoverResult.anchor; + const parts = hoverResult.hoverParts; + this._renderedHoverParts = this._register(new RenderedContentHoverParts( + editor, + participants, + parts, + keybindingService, + context + )); + const { showAtPosition, showAtSecondaryPosition } = RenderedContentHover.computeHoverPositions(editor, anchor.range, parts); + this.shouldAppearBeforeContent = parts.some(m => m.isBeforeContent); + this.showAtPosition = showAtPosition; + this.showAtSecondaryPosition = showAtSecondaryPosition; + this.initialMousePosX = anchor.initialMousePosX; + this.initialMousePosY = anchor.initialMousePosY; + this.shouldFocus = computer.shouldFocus; + this.source = computer.source; + } + + public get domNode(): DocumentFragment { + return this._renderedHoverParts.domNode; + } + + public get domNodeHasChildren(): boolean { + return this._renderedHoverParts.domNodeHasChildren; + } + + public get focusedHoverPartIndex(): number { + return this._renderedHoverParts.focusedHoverPartIndex; + } + + public focusHoverPartWithIndex(index: number): void { + this._renderedHoverParts.focusHoverPartWithIndex(index); + } + + public getAccessibleWidgetContent(): string { + return this._renderedHoverParts.getAccessibleContent(); + } + + public getAccessibleWidgetContentAtIndex(index: number): string { + return this._renderedHoverParts.getAccessibleHoverContentAtIndex(index); + } + + public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise { + this._renderedHoverParts.updateHoverVerbosityLevel(action, index, focus); + } + + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._renderedHoverParts.doesHoverAtIndexSupportVerbosityAction(index, action); + } + + public isColorPickerVisible(): boolean { + return this._renderedHoverParts.isColorPickerVisible(); + } + + public static computeHoverPositions(editor: ICodeEditor, anchorRange: Range, hoverParts: IHoverPart[]): { showAtPosition: Position; showAtSecondaryPosition: Position } { + + let startColumnBoundary = 1; + if (editor.hasModel()) { + // Ensure the range is on the current view line + const viewModel = editor._getViewModel(); + const coordinatesConverter = viewModel.coordinatesConverter; + const anchorViewRange = coordinatesConverter.convertModelRangeToViewRange(anchorRange); + const anchorViewMinColumn = viewModel.getLineMinColumn(anchorViewRange.startLineNumber); + const anchorViewRangeStart = new Position(anchorViewRange.startLineNumber, anchorViewMinColumn); + startColumnBoundary = coordinatesConverter.convertViewPositionToModelPosition(anchorViewRangeStart).column; + } + + // The anchor range is always on a single line + const anchorStartLineNumber = anchorRange.startLineNumber; + let secondaryPositionColumn = anchorRange.startColumn; + let forceShowAtRange: Range | undefined; + + for (const hoverPart of hoverParts) { + const hoverPartRange = hoverPart.range; + const hoverPartRangeOnAnchorStartLine = hoverPartRange.startLineNumber === anchorStartLineNumber; + const hoverPartRangeOnAnchorEndLine = hoverPartRange.endLineNumber === anchorStartLineNumber; + const hoverPartRangeIsOnAnchorLine = hoverPartRangeOnAnchorStartLine && hoverPartRangeOnAnchorEndLine; + if (hoverPartRangeIsOnAnchorLine) { + // this message has a range that is completely sitting on the line of the anchor + const hoverPartStartColumn = hoverPartRange.startColumn; + const minSecondaryPositionColumn = Math.min(secondaryPositionColumn, hoverPartStartColumn); + secondaryPositionColumn = Math.max(minSecondaryPositionColumn, startColumnBoundary); + } + if (hoverPart.forceShowAtRange) { + forceShowAtRange = hoverPartRange; + } + } + + let showAtPosition: Position; + let showAtSecondaryPosition: Position; + if (forceShowAtRange) { + const forceShowAtPosition = forceShowAtRange.getStartPosition(); + showAtPosition = forceShowAtPosition; + showAtSecondaryPosition = forceShowAtPosition; + } else { + showAtPosition = anchorRange.getStartPosition(); + showAtSecondaryPosition = new Position(anchorStartLineNumber, secondaryPositionColumn); + } + return { + showAtPosition, + showAtSecondaryPosition, + }; + } +} + +interface IRenderedContentHoverPart { + /** + * Type of rendered part + */ + type: 'hoverPart'; + /** + * Participant of the rendered hover part + */ + participant: IEditorHoverParticipant; + /** + * The rendered hover part + */ + hoverPart: IHoverPart; + /** + * The HTML element containing the hover status bar. + */ + hoverElement: HTMLElement; +} + +interface IRenderedContentStatusBar { + /** + * Type of rendered part + */ + type: 'statusBar'; + /** + * The HTML element containing the hover status bar. + */ + hoverElement: HTMLElement; + /** + * The actions of the hover status bar. + */ + actions: HoverAction[]; +} + +type IRenderedContentHoverPartOrStatusBar = IRenderedContentHoverPart | IRenderedContentStatusBar; + +class RenderedStatusBar implements IDisposable { + + constructor(fragment: DocumentFragment, private readonly _statusBar: EditorHoverStatusBar) { + fragment.appendChild(this._statusBar.hoverElement); + } + + get hoverElement(): HTMLElement { + return this._statusBar.hoverElement; + } + + get actions(): HoverAction[] { + return this._statusBar.actions; + } + + dispose() { + this._statusBar.dispose(); + } +} + +class RenderedContentHoverParts extends Disposable { + + private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ + description: 'content-hover-highlight', + className: 'hoverHighlight' + }); + + private readonly _renderedParts: IRenderedContentHoverPartOrStatusBar[] = []; + private readonly _fragment: DocumentFragment; + private readonly _context: IEditorHoverContext; + + private _markdownHoverParticipant: MarkdownHoverParticipant | undefined; + private _colorHoverParticipant: ColorHoverParticipant | undefined; + private _focusedHoverPartIndex: number = -1; + + constructor( + editor: ICodeEditor, + participants: IEditorHoverParticipant[], + hoverParts: IHoverPart[], + keybindingService: IKeybindingService, + context: IEditorHoverContext + ) { + super(); + this._context = context; + this._fragment = document.createDocumentFragment(); + this._register(this._renderParts(participants, hoverParts, context, keybindingService)); + this._register(this._registerListenersOnRenderedParts()); + this._register(this._createEditorDecorations(editor, hoverParts)); + this._updateMarkdownAndColorParticipantInfo(participants); + } + + private _createEditorDecorations(editor: ICodeEditor, hoverParts: IHoverPart[]): IDisposable { + if (hoverParts.length === 0) { + return Disposable.None; + } + let highlightRange = hoverParts[0].range; + for (const hoverPart of hoverParts) { + const hoverPartRange = hoverPart.range; + highlightRange = Range.plusRange(highlightRange, hoverPartRange); + } + const highlightDecoration = editor.createDecorationsCollection(); + highlightDecoration.set([{ + range: highlightRange, + options: RenderedContentHoverParts._DECORATION_OPTIONS + }]); + return toDisposable(() => { + highlightDecoration.clear(); + }); + } + + private _renderParts(participants: IEditorHoverParticipant[], hoverParts: IHoverPart[], hoverContext: IEditorHoverContext, keybindingService: IKeybindingService): IDisposable { + const statusBar = new EditorHoverStatusBar(keybindingService); + const hoverRenderingContext: IEditorHoverRenderContext = { + fragment: this._fragment, + statusBar, + ...hoverContext + }; + const disposables = new DisposableStore(); + for (const participant of participants) { + const renderedHoverParts = this._renderHoverPartsForParticipant(hoverParts, participant, hoverRenderingContext); + disposables.add(renderedHoverParts); + for (const renderedHoverPart of renderedHoverParts.renderedHoverParts) { + this._renderedParts.push({ + type: 'hoverPart', + participant, + hoverPart: renderedHoverPart.hoverPart, + hoverElement: renderedHoverPart.hoverElement, + }); + } + } + const renderedStatusBar = this._renderStatusBar(this._fragment, statusBar); + if (renderedStatusBar) { + disposables.add(renderedStatusBar); + this._renderedParts.push({ + type: 'statusBar', + hoverElement: renderedStatusBar.hoverElement, + actions: renderedStatusBar.actions, + }); + } + return toDisposable(() => { disposables.dispose(); }); + } + + private _renderHoverPartsForParticipant(hoverParts: IHoverPart[], participant: IEditorHoverParticipant, hoverRenderingContext: IEditorHoverRenderContext): IRenderedHoverParts { + const hoverPartsForParticipant = hoverParts.filter(hoverPart => hoverPart.owner === participant); + const hasHoverPartsForParticipant = hoverPartsForParticipant.length > 0; + if (!hasHoverPartsForParticipant) { + return new RenderedHoverParts([]); + } + return participant.renderHoverParts(hoverRenderingContext, hoverPartsForParticipant); + } + + private _renderStatusBar(fragment: DocumentFragment, statusBar: EditorHoverStatusBar): RenderedStatusBar | undefined { + if (!statusBar.hasContent) { + return undefined; + } + return new RenderedStatusBar(fragment, statusBar); + } + + private _registerListenersOnRenderedParts(): IDisposable { + const disposables = new DisposableStore(); + this._renderedParts.forEach((renderedPart: IRenderedContentHoverPartOrStatusBar, index: number) => { + const element = renderedPart.hoverElement; + element.tabIndex = 0; + disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_IN, (event: Event) => { + event.stopPropagation(); + this._focusedHoverPartIndex = index; + })); + disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_OUT, (event: Event) => { + event.stopPropagation(); + this._focusedHoverPartIndex = -1; + })); + }); + return disposables; + } + + private _updateMarkdownAndColorParticipantInfo(participants: IEditorHoverParticipant[]) { + const markdownHoverParticipant = participants.find(p => { + return (p instanceof MarkdownHoverParticipant) && !(p instanceof InlayHintsHover); + }); + if (markdownHoverParticipant) { + this._markdownHoverParticipant = markdownHoverParticipant as MarkdownHoverParticipant; + } + this._colorHoverParticipant = participants.find(p => p instanceof ColorHoverParticipant); + } + + public focusHoverPartWithIndex(index: number): void { + if (index < 0 || index >= this._renderedParts.length) { + return; + } + this._renderedParts[index].hoverElement.focus(); + } + + public getAccessibleContent(): string { + const content: string[] = []; + for (let i = 0; i < this._renderedParts.length; i++) { + content.push(this.getAccessibleHoverContentAtIndex(i)); + } + return content.join('\n\n'); + } + + public getAccessibleHoverContentAtIndex(index: number): string { + const renderedPart = this._renderedParts[index]; + if (!renderedPart) { + return ''; + } + if (renderedPart.type === 'statusBar') { + const statusBarDescription = [localize('hoverAccessibilityStatusBar', "This is a hover status bar.")]; + for (const action of renderedPart.actions) { + const keybinding = action.actionKeybindingLabel; + if (keybinding) { + statusBarDescription.push(localize('hoverAccessibilityStatusBarActionWithKeybinding', "It has an action with label {0} and keybinding {1}.", action.actionLabel, keybinding)); + } else { + statusBarDescription.push(localize('hoverAccessibilityStatusBarActionWithoutKeybinding', "It has an action with label {0}.", action.actionLabel)); + } + } + return statusBarDescription.join('\n'); + } + return renderedPart.participant.getAccessibleContent(renderedPart.hoverPart); + } + + public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise { + if (!this._markdownHoverParticipant) { + return; + } + const normalizedMarkdownHoverIndex = this._normalizedIndexToMarkdownHoverIndexRange(this._markdownHoverParticipant, index); + if (normalizedMarkdownHoverIndex === undefined) { + return; + } + const renderedPart = await this._markdownHoverParticipant.updateMarkdownHoverVerbosityLevel(action, normalizedMarkdownHoverIndex, focus); + if (!renderedPart) { + return; + } + this._renderedParts[index] = { + type: 'hoverPart', + participant: this._markdownHoverParticipant, + hoverPart: renderedPart.hoverPart, + hoverElement: renderedPart.hoverElement, + }; + this._context.onContentsChanged(); + } + + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + if (!this._markdownHoverParticipant) { + return false; + } + const normalizedMarkdownHoverIndex = this._normalizedIndexToMarkdownHoverIndexRange(this._markdownHoverParticipant, index); + if (normalizedMarkdownHoverIndex === undefined) { + return false; + } + return this._markdownHoverParticipant.doesMarkdownHoverAtIndexSupportVerbosityAction(normalizedMarkdownHoverIndex, action); + } + + public isColorPickerVisible(): boolean { + return this._colorHoverParticipant?.isColorPickerVisible() ?? false; + } + + private _normalizedIndexToMarkdownHoverIndexRange(markdownHoverParticipant: MarkdownHoverParticipant, index: number): number | undefined { + const renderedPart = this._renderedParts[index]; + if (!renderedPart || renderedPart.type !== 'hoverPart') { + return undefined; + } + const isHoverPartMarkdownHover = renderedPart.participant === markdownHoverParticipant; + if (!isHoverPartMarkdownHover) { + return undefined; + } + const firstIndexOfMarkdownHovers = this._renderedParts.findIndex(renderedPart => + renderedPart.type === 'hoverPart' + && renderedPart.participant === markdownHoverParticipant + ); + if (firstIndexOfMarkdownHovers === -1) { + throw new BugIndicatingError(); + } + return index - firstIndexOfMarkdownHovers; + } + + public get domNode(): DocumentFragment { + return this._fragment; + } + + public get domNodeHasChildren(): boolean { + return this._fragment.hasChildNodes(); + } + + public get focusedHoverPartIndex(): number { + return this._focusedHoverPartIndex; + } +} diff --git a/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts b/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts index 04d84593d06..bb43959419b 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts @@ -13,6 +13,8 @@ const $ = dom.$; export class EditorHoverStatusBar extends Disposable implements IEditorHoverStatusBar { public readonly hoverElement: HTMLElement; + public readonly actions: HoverAction[] = []; + private readonly actionsElement: HTMLElement; private _hasContent: boolean = false; @@ -39,7 +41,9 @@ export class EditorHoverStatusBar extends Disposable implements IEditorHoverStat const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); const keybindingLabel = keybinding ? keybinding.getLabel() : null; this._hasContent = true; - return this._register(HoverAction.render(this.actionsElement, actionOptions, keybindingLabel)); + const action = this._register(HoverAction.render(this.actionsElement, actionOptions, keybindingLabel)); + this.actions.push(action); + return action; } public append(element: HTMLElement): HTMLElement { diff --git a/src/vs/editor/contrib/hover/browser/contentHoverTypes.ts b/src/vs/editor/contrib/hover/browser/contentHoverTypes.ts index 5d32dc83560..a52d758baa7 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverTypes.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverTypes.ts @@ -3,25 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { Position } from 'vs/editor/common/core/position'; -import { HoverStartSource } from 'vs/editor/contrib/hover/browser/hoverOperation'; -import { HoverAnchor, IEditorHoverColorPickerWidget, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; export class HoverResult { constructor( public readonly anchor: HoverAnchor, - public readonly messages: IHoverPart[], + public readonly hoverParts: IHoverPart[], public readonly isComplete: boolean ) { } public filter(anchor: HoverAnchor): HoverResult { - const filteredMessages = this.messages.filter((m) => m.isValidForHoverAnchor(anchor)); - if (filteredMessages.length === this.messages.length) { + const filteredHoverParts = this.hoverParts.filter((m) => m.isValidForHoverAnchor(anchor)); + if (filteredHoverParts.length === this.hoverParts.length) { return this; } - return new FilteredHoverResult(this, this.anchor, filteredMessages, this.isComplete); + return new FilteredHoverResult(this, this.anchor, filteredHoverParts, this.isComplete); } } @@ -40,21 +37,3 @@ export class FilteredHoverResult extends HoverResult { return this.original.filter(anchor); } } - -export class ContentHoverVisibleData { - - public closestMouseDistance: number | undefined = undefined; - - constructor( - public initialMousePosX: number | undefined, - public initialMousePosY: number | undefined, - public readonly colorPicker: IEditorHoverColorPickerWidget | null, - public readonly showAtPosition: Position, - public readonly showAtSecondaryPosition: Position, - public readonly preferAbove: boolean, - public readonly stoleFocus: boolean, - public readonly source: HoverStartSource, - public readonly isBeforeContent: boolean, - public readonly disposables: DisposableStore - ) { } -} diff --git a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts index 58903400914..37cf8740e37 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts @@ -15,7 +15,8 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { getHoverAccessibleViewHint, HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; import { PositionAffinity } from 'vs/editor/common/model'; -import { ContentHoverVisibleData } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; +import { Emitter } from 'vs/base/common/event'; +import { RenderedContentHover } from 'vs/editor/contrib/hover/browser/contentHoverRendered'; const HORIZONTAL_SCROLLING_BY = 30; const CONTAINER_HEIGHT_PADDING = 6; @@ -25,7 +26,7 @@ export class ContentHoverWidget extends ResizableContentWidget { public static ID = 'editor.contrib.resizableContentHoverWidget'; private static _lastDimensions: dom.Dimension = new dom.Dimension(0, 0); - private _visibleData: ContentHoverVisibleData | undefined; + private _renderedHover: RenderedContentHover | undefined; private _positionPreference: ContentWidgetPositionPreference | undefined; private _minimumSize: dom.Dimension; private _contentWidth: number | undefined; @@ -34,12 +35,11 @@ export class ContentHoverWidget extends ResizableContentWidget { private readonly _hoverVisibleKey: IContextKey; private readonly _hoverFocusedKey: IContextKey; - public get isColorPickerVisible(): boolean { - return Boolean(this._visibleData?.colorPicker); - } + private readonly _onDidResize = this._register(new Emitter()); + public readonly onDidResize = this._onDidResize.event; public get isVisibleFromKeyboard(): boolean { - return (this._visibleData?.source === HoverStartSource.Keyboard); + return (this._renderedHover?.source === HoverStartSource.Keyboard); } public get isVisible(): boolean { @@ -86,13 +86,13 @@ export class ContentHoverWidget extends ResizableContentWidget { this._register(focusTracker.onDidBlur(() => { this._hoverFocusedKey.set(false); })); - this._setHoverData(undefined); + this._setRenderedHover(undefined); this._editor.addContentWidget(this); } public override dispose(): void { super.dispose(); - this._visibleData?.disposables.dispose(); + this._renderedHover?.dispose(); this._editor.removeContentWidget(this); } @@ -158,11 +158,11 @@ export class ContentHoverWidget extends ResizableContentWidget { this._updateResizableNodeMaxDimensions(); this._hover.scrollbar.scanDomNode(); this._editor.layoutContentWidget(this); - this._visibleData?.colorPicker?.layout(); + this._onDidResize.fire(); } private _findAvailableSpaceVertically(): number | undefined { - const position = this._visibleData?.showAtPosition; + const position = this._renderedHover?.showAtPosition; if (!position) { return; } @@ -223,23 +223,20 @@ export class ContentHoverWidget extends ResizableContentWidget { public isMouseGettingCloser(posx: number, posy: number): boolean { - if (!this._visibleData) { + if (!this._renderedHover) { return false; } - if ( - typeof this._visibleData.initialMousePosX === 'undefined' - || typeof this._visibleData.initialMousePosY === 'undefined' - ) { - this._visibleData.initialMousePosX = posx; - this._visibleData.initialMousePosY = posy; + if (this._renderedHover.initialMousePosX === undefined || this._renderedHover.initialMousePosY === undefined) { + this._renderedHover.initialMousePosX = posx; + this._renderedHover.initialMousePosY = posy; return false; } const widgetRect = dom.getDomNodePagePosition(this.getDomNode()); - if (typeof this._visibleData.closestMouseDistance === 'undefined') { - this._visibleData.closestMouseDistance = computeDistanceFromPointToRectangle( - this._visibleData.initialMousePosX, - this._visibleData.initialMousePosY, + if (this._renderedHover.closestMouseDistance === undefined) { + this._renderedHover.closestMouseDistance = computeDistanceFromPointToRectangle( + this._renderedHover.initialMousePosX, + this._renderedHover.initialMousePosY, widgetRect.left, widgetRect.top, widgetRect.width, @@ -255,20 +252,20 @@ export class ContentHoverWidget extends ResizableContentWidget { widgetRect.width, widgetRect.height ); - if (distance > this._visibleData.closestMouseDistance + 4 /* tolerance of 4 pixels */) { + if (distance > this._renderedHover.closestMouseDistance + 4 /* tolerance of 4 pixels */) { // The mouse is getting farther away return false; } - this._visibleData.closestMouseDistance = Math.min(this._visibleData.closestMouseDistance, distance); + this._renderedHover.closestMouseDistance = Math.min(this._renderedHover.closestMouseDistance, distance); return true; } - private _setHoverData(hoverData: ContentHoverVisibleData | undefined): void { - this._visibleData?.disposables.dispose(); - this._visibleData = hoverData; - this._hoverVisibleKey.set(!!hoverData); - this._hover.containerDomNode.classList.toggle('hidden', !hoverData); + private _setRenderedHover(renderedHover: RenderedContentHover | undefined): void { + this._renderedHover?.dispose(); + this._renderedHover = renderedHover; + this._hoverVisibleKey.set(!!renderedHover); + this._hover.containerDomNode.classList.toggle('hidden', !renderedHover); } private _updateFont(): void { @@ -298,10 +295,10 @@ export class ContentHoverWidget extends ResizableContentWidget { this._setHoverWidgetMaxDimensions(width, height); } - private _render(node: DocumentFragment, hoverData: ContentHoverVisibleData) { - this._setHoverData(hoverData); + private _render(renderedHover: RenderedContentHover) { + this._setRenderedHover(renderedHover); this._updateFont(); - this._updateContent(node); + this._updateContent(renderedHover.domNode); this._updateMaxDimensions(); this.onContentsChanged(); // Simply force a synchronous render on the editor @@ -310,33 +307,33 @@ export class ContentHoverWidget extends ResizableContentWidget { } override getPosition(): IContentWidgetPosition | null { - if (!this._visibleData) { + if (!this._renderedHover) { return null; } return { - position: this._visibleData.showAtPosition, - secondaryPosition: this._visibleData.showAtSecondaryPosition, - positionAffinity: this._visibleData.isBeforeContent ? PositionAffinity.LeftOfInjectedText : undefined, + position: this._renderedHover.showAtPosition, + secondaryPosition: this._renderedHover.showAtSecondaryPosition, + positionAffinity: this._renderedHover.shouldAppearBeforeContent ? PositionAffinity.LeftOfInjectedText : undefined, preference: [this._positionPreference ?? ContentWidgetPositionPreference.ABOVE] }; } - public showAt(node: DocumentFragment, hoverData: ContentHoverVisibleData): void { + public show(renderedHover: RenderedContentHover): void { if (!this._editor || !this._editor.hasModel()) { return; } - this._render(node, hoverData); + this._render(renderedHover); const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode); - const widgetPosition = hoverData.showAtPosition; + const widgetPosition = renderedHover.showAtPosition; this._positionPreference = this._findPositionPreference(widgetHeight, widgetPosition) ?? ContentWidgetPositionPreference.ABOVE; // See https://github.com/microsoft/vscode/issues/140339 // TODO: Doing a second layout of the hover after force rendering the editor this.onContentsChanged(); - if (hoverData.stoleFocus) { + if (renderedHover.shouldFocus) { this._hover.containerDomNode.focus(); } - hoverData.colorPicker?.layout(); + this._onDidResize.fire(); // The aria label overrides the label, so if we add to it, add the contents of the hover const hoverFocused = this._hover.containerDomNode.ownerDocument.activeElement === this._hover.containerDomNode; const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint( @@ -350,16 +347,16 @@ export class ContentHoverWidget extends ResizableContentWidget { } public hide(): void { - if (!this._visibleData) { + if (!this._renderedHover) { return; } - const stoleFocus = this._visibleData.stoleFocus || this._hoverFocusedKey.get(); - this._setHoverData(undefined); + const hoverStoleFocus = this._renderedHover.shouldFocus || this._hoverFocusedKey.get(); + this._setRenderedHover(undefined); this._resizableNode.maxSize = new dom.Dimension(Infinity, Infinity); this._resizableNode.clearSashHoverState(); this._hoverFocusedKey.set(false); this._editor.layoutContentWidget(this); - if (stoleFocus) { + if (hoverStoleFocus) { this._editor.focus(); } } @@ -406,9 +403,9 @@ export class ContentHoverWidget extends ResizableContentWidget { this._updateMinimumWidth(); this._resizableNode.layout(height, width); - if (this._visibleData?.showAtPosition) { + if (this._renderedHover?.showAtPosition) { const widgetHeight = dom.getTotalHeight(this._hover.containerDomNode); - this._positionPreference = this._findPositionPreference(widgetHeight, this._visibleData.showAtPosition); + this._positionPreference = this._findPositionPreference(widgetHeight, this._renderedHover.showAtPosition); } this._layoutContentWidget(); } diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index 3c21b4edf84..2520856e59d 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -7,6 +7,12 @@ background-color: var(--vscode-editor-hoverHighlightBackground); } +.monaco-editor .monaco-hover-content { + padding-right: 2px; + padding-bottom: 2px; + box-sizing: border-box; +} + .monaco-editor .monaco-hover { color: var(--vscode-editorHoverWidget-foreground); background-color: var(--vscode-editorHoverWidget-background); diff --git a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts index 557f1fffa85..41a7fbdfce9 100644 --- a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts +++ b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts @@ -2,48 +2,257 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { localize } from 'vs/nls'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; -import { AccessibleViewType, AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewType, AccessibleViewProviderId, AdvancedContentProvider, IAccessibleViewContentProvider, IAccessibleViewOptions } from 'vs/platform/accessibility/browser/accessibleView'; import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { HoverVerbosityAction } from 'vs/editor/common/languages'; +import { DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID, DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { Action, IAction } from 'vs/base/common/actions'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { labelForHoverVerbosityAction } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; + +namespace HoverAccessibilityHelpNLS { + export const introHoverPart = localize('introHoverPart', 'The focused hover part content is the following:'); + export const introHoverFull = localize('introHoverFull', 'The full focused hover content is the following:'); + export const increaseVerbosity = localize('increaseVerbosity', '- The focused hover part verbosity level can be increased with the Increase Hover Verbosity command.', INCREASE_HOVER_VERBOSITY_ACTION_ID); + export const decreaseVerbosity = localize('decreaseVerbosity', '- The focused hover part verbosity level can be decreased with the Decrease Hover Verbosity command.', DECREASE_HOVER_VERBOSITY_ACTION_ID); +} export class HoverAccessibleView implements IAccessibleViewImplentation { - readonly type = AccessibleViewType.View; - readonly priority = 95; - readonly name = 'hover'; - readonly when = EditorContextKeys.hoverFocused; - getProvider(accessor: ServicesAccessor) { + + public readonly type = AccessibleViewType.View; + public readonly priority = 95; + public readonly name = 'hover'; + public readonly when = EditorContextKeys.hoverFocused; + + private _provider: HoverAccessibleViewProvider | undefined; + + getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined { const codeEditorService = accessor.get(ICodeEditorService); - const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - const editorHoverContent = editor ? HoverController.get(editor)?.getWidgetContent() ?? undefined : undefined; - if (!editor || !editorHoverContent) { + const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!codeEditor) { + throw new Error('No active or focused code editor'); + } + const hoverController = HoverController.get(codeEditor); + if (!hoverController) { return; } - return { - id: AccessibleViewProviderId.Hover, - verbositySettingKey: 'accessibility.verbosity.hover', - provideContent() { return editorHoverContent; }, - onClose() { - HoverController.get(editor)?.focus(); - }, - options: { - language: editor?.getModel()?.getLanguageId() ?? 'typescript', - type: AccessibleViewType.View + const keybindingService = accessor.get(IKeybindingService); + this._provider = accessor.get(IInstantiationService).createInstance(HoverAccessibleViewProvider, keybindingService, codeEditor, hoverController); + return this._provider; + } + + dispose(): void { + this._provider?.dispose(); + } +} + +export class HoverAccessibilityHelp implements IAccessibleViewImplentation { + + public readonly priority = 100; + public readonly name = 'hover'; + public readonly type = AccessibleViewType.Help; + public readonly when = EditorContextKeys.hoverVisible; + + private _provider: HoverAccessibleViewProvider | undefined; + + getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined { + const codeEditorService = accessor.get(ICodeEditorService); + const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!codeEditor) { + throw new Error('No active or focused code editor'); + } + const hoverController = HoverController.get(codeEditor); + if (!hoverController) { + return; + } + return accessor.get(IInstantiationService).createInstance(HoverAccessibilityHelpProvider, hoverController); + } + + dispose(): void { + this._provider?.dispose(); + } +} + +abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAccessibleViewContentProvider { + + abstract provideContent(): string; + abstract options: IAccessibleViewOptions; + + public readonly id = AccessibleViewProviderId.Hover; + public readonly verbositySettingKey = 'accessibility.verbosity.hover'; + + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + public readonly onDidChangeContent: Event = this._onDidChangeContent.event; + + protected _focusedHoverPartIndex: number = -1; + + constructor(protected readonly _hoverController: HoverController) { + super(); + } + + public onOpen(): void { + if (!this._hoverController) { + return; + } + this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = true; + this._focusedHoverPartIndex = this._hoverController.focusedHoverPartIndex(); + this._register(this._hoverController.onHoverContentsChanged(() => { + this._onDidChangeContent.fire(); + })); + } + + public onClose(): void { + if (!this._hoverController) { + return; + } + if (this._focusedHoverPartIndex === -1) { + this._hoverController.focus(); + } else { + this._hoverController.focusHoverPartWithIndex(this._focusedHoverPartIndex); + } + this._focusedHoverPartIndex = -1; + this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = false; + this.dispose(); + } + + provideContentAtIndex(focusedHoverIndex: number, includeVerbosityActions: boolean): string { + if (focusedHoverIndex !== -1) { + const accessibleContent = this._hoverController.getAccessibleWidgetContentAtIndex(focusedHoverIndex); + if (accessibleContent === undefined) { + return ''; } - }; + const contents: string[] = []; + if (includeVerbosityActions) { + contents.push(...this._descriptionsOfVerbosityActionsForIndex(focusedHoverIndex)); + } + contents.push(HoverAccessibilityHelpNLS.introHoverPart); + contents.push(accessibleContent); + return contents.join('\n\n'); + } else { + const accessibleContent = this._hoverController.getAccessibleWidgetContent(); + if (accessibleContent === undefined) { + return ''; + } + const contents: string[] = []; + contents.push(HoverAccessibilityHelpNLS.introHoverFull); + contents.push(accessibleContent); + return contents.join('\n\n'); + } + } + + private _descriptionsOfVerbosityActionsForIndex(index: number): string[] { + const content: string[] = []; + const descriptionForIncreaseAction = this._descriptionOfVerbosityActionForIndex(HoverVerbosityAction.Increase, index); + if (descriptionForIncreaseAction !== undefined) { + content.push(descriptionForIncreaseAction); + } + const descriptionForDecreaseAction = this._descriptionOfVerbosityActionForIndex(HoverVerbosityAction.Decrease, index); + if (descriptionForDecreaseAction !== undefined) { + content.push(descriptionForDecreaseAction); + } + return content; + } + + private _descriptionOfVerbosityActionForIndex(action: HoverVerbosityAction, index: number): string | undefined { + const isActionSupported = this._hoverController.doesHoverAtIndexSupportVerbosityAction(index, action); + if (!isActionSupported) { + return; + } + switch (action) { + case HoverVerbosityAction.Increase: + return HoverAccessibilityHelpNLS.increaseVerbosity; + case HoverVerbosityAction.Decrease: + return HoverAccessibilityHelpNLS.decreaseVerbosity; + } + } +} + +export class HoverAccessibilityHelpProvider extends BaseHoverAccessibleViewProvider implements IAccessibleViewContentProvider { + + public readonly options: IAccessibleViewOptions = { type: AccessibleViewType.Help }; + + constructor(hoverController: HoverController) { + super(hoverController); + } + + provideContent(): string { + return this.provideContentAtIndex(this._focusedHoverPartIndex, true); + } +} + +export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider implements IAccessibleViewContentProvider { + + public readonly options: IAccessibleViewOptions = { type: AccessibleViewType.View }; + + constructor( + private readonly _keybindingService: IKeybindingService, + private readonly _editor: ICodeEditor, + hoverController: HoverController, + ) { + super(hoverController); + this._initializeOptions(this._editor, hoverController); + } + + public provideContent(): string { + return this.provideContentAtIndex(this._focusedHoverPartIndex, false); + } + + public get actions(): IAction[] { + const actions: IAction[] = []; + actions.push(this._getActionFor(this._editor, HoverVerbosityAction.Increase)); + actions.push(this._getActionFor(this._editor, HoverVerbosityAction.Decrease)); + return actions; + } + + private _getActionFor(editor: ICodeEditor, action: HoverVerbosityAction): IAction { + let actionId: string; + let accessibleActionId: string; + let actionCodicon: ThemeIcon; + switch (action) { + case HoverVerbosityAction.Increase: + actionId = INCREASE_HOVER_VERBOSITY_ACTION_ID; + accessibleActionId = INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID; + actionCodicon = Codicon.add; + break; + case HoverVerbosityAction.Decrease: + actionId = DECREASE_HOVER_VERBOSITY_ACTION_ID; + accessibleActionId = DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID; + actionCodicon = Codicon.remove; + break; + } + const actionLabel = labelForHoverVerbosityAction(this._keybindingService, action); + const actionEnabled = this._hoverController.doesHoverAtIndexSupportVerbosityAction(this._focusedHoverPartIndex, action); + return new Action(accessibleActionId, actionLabel, ThemeIcon.asClassName(actionCodicon), actionEnabled, () => { + editor.getAction(actionId)?.run({ index: this._focusedHoverPartIndex, focus: false }); + }); + } + + private _initializeOptions(editor: ICodeEditor, hoverController: HoverController): void { + const helpProvider = this._register(new HoverAccessibilityHelpProvider(hoverController)); + this.options.language = editor.getModel()?.getLanguageId(); + this.options.customHelp = () => { return helpProvider.provideContentAtIndex(this._focusedHoverPartIndex, true); }; } } export class ExtHoverAccessibleView implements IAccessibleViewImplentation { - readonly type = AccessibleViewType.View; - readonly priority = 90; - readonly name = 'extension-hover'; - getProvider(accessor: ServicesAccessor) { + + public readonly type = AccessibleViewType.View; + public readonly priority = 90; + public readonly name = 'extension-hover'; + + getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined { const contextViewService = accessor.get(IContextViewService); const contextViewElement = contextViewService.getContextViewElement(); const extensionHoverContent = contextViewElement?.textContent ?? undefined; @@ -63,4 +272,6 @@ export class ExtHoverAccessibleView implements IAccessibleViewImplentation { options: { language: 'typescript', type: AccessibleViewType.View } }; } + + dispose() { } } diff --git a/src/vs/editor/contrib/hover/browser/hoverActionIds.ts b/src/vs/editor/contrib/hover/browser/hoverActionIds.ts index 5cc42e1aa50..2ade6360ac1 100644 --- a/src/vs/editor/contrib/hover/browser/hoverActionIds.ts +++ b/src/vs/editor/contrib/hover/browser/hoverActionIds.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; export const SHOW_OR_FOCUS_HOVER_ACTION_ID = 'editor.action.showHover'; export const SHOW_DEFINITION_PREVIEW_HOVER_ACTION_ID = 'editor.action.showDefinitionPreviewHover'; @@ -14,4 +15,8 @@ export const PAGE_DOWN_HOVER_ACTION_ID = 'editor.action.pageDownHover'; export const GO_TO_TOP_HOVER_ACTION_ID = 'editor.action.goToTopHover'; export const GO_TO_BOTTOM_HOVER_ACTION_ID = 'editor.action.goToBottomHover'; export const INCREASE_HOVER_VERBOSITY_ACTION_ID = 'editor.action.increaseHoverVerbosityLevel'; +export const INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID = 'editor.action.increaseHoverVerbosityLevelFromAccessibleView'; +export const INCREASE_HOVER_VERBOSITY_ACTION_LABEL = nls.localize({ key: 'increaseHoverVerbosityLevel', comment: ['Label for action that will increase the hover verbosity level.'] }, "Increase Hover Verbosity Level"); export const DECREASE_HOVER_VERBOSITY_ACTION_ID = 'editor.action.decreaseHoverVerbosityLevel'; +export const DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID = 'editor.action.decreaseHoverVerbosityLevelFromAccessibleView'; +export const DECREASE_HOVER_VERBOSITY_ACTION_LABEL = nls.localize({ key: 'decreaseHoverVerbosityLevel', comment: ['Label for action that will decrease the hover verbosity level.'] }, "Decrease Hover Verbosity Level"); diff --git a/src/vs/editor/contrib/hover/browser/hoverActions.ts b/src/vs/editor/contrib/hover/browser/hoverActions.ts index 9654e7c3d09..37eea40441a 100644 --- a/src/vs/editor/contrib/hover/browser/hoverActions.ts +++ b/src/vs/editor/contrib/hover/browser/hoverActions.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DECREASE_HOVER_VERBOSITY_ACTION_ID, GO_TO_BOTTOM_HOVER_ACTION_ID, GO_TO_TOP_HOVER_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, PAGE_DOWN_HOVER_ACTION_ID, PAGE_UP_HOVER_ACTION_ID, SCROLL_DOWN_HOVER_ACTION_ID, SCROLL_LEFT_HOVER_ACTION_ID, SCROLL_RIGHT_HOVER_ACTION_ID, SCROLL_UP_HOVER_ACTION_ID, SHOW_DEFINITION_PREVIEW_HOVER_ACTION_ID, SHOW_OR_FOCUS_HOVER_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds'; +import { DECREASE_HOVER_VERBOSITY_ACTION_ID, DECREASE_HOVER_VERBOSITY_ACTION_LABEL, GO_TO_BOTTOM_HOVER_ACTION_ID, GO_TO_TOP_HOVER_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_LABEL, PAGE_DOWN_HOVER_ACTION_ID, PAGE_UP_HOVER_ACTION_ID, SCROLL_DOWN_HOVER_ACTION_ID, SCROLL_LEFT_HOVER_ACTION_ID, SCROLL_RIGHT_HOVER_ACTION_ID, SCROLL_UP_HOVER_ACTION_ID, SHOW_DEFINITION_PREVIEW_HOVER_ACTION_ID, SHOW_OR_FOCUS_HOVER_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; @@ -425,17 +425,19 @@ export class IncreaseHoverVerbosityLevel extends EditorAction { constructor() { super({ id: INCREASE_HOVER_VERBOSITY_ACTION_ID, - label: nls.localize({ - key: 'increaseHoverVerbosityLevel', - comment: ['Label for action that will increase the hover verbosity level.'] - }, "Increase Hover Verbosity Level"), + label: INCREASE_HOVER_VERBOSITY_ACTION_LABEL, alias: 'Increase Hover Verbosity Level', - precondition: EditorContextKeys.hoverFocused + precondition: EditorContextKeys.hoverVisible }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - HoverController.get(editor)?.updateFocusedMarkdownHoverVerbosityLevel(HoverVerbosityAction.Increase); + public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void { + const hoverController = HoverController.get(editor); + if (!hoverController) { + return; + } + const index = args?.index !== undefined ? args.index : hoverController.focusedHoverPartIndex(); + hoverController.updateHoverVerbosityLevel(HoverVerbosityAction.Increase, index, args?.focus); } } @@ -444,16 +446,18 @@ export class DecreaseHoverVerbosityLevel extends EditorAction { constructor() { super({ id: DECREASE_HOVER_VERBOSITY_ACTION_ID, - label: nls.localize({ - key: 'decreaseHoverVerbosityLevel', - comment: ['Label for action that will decrease the hover verbosity level.'] - }, "Decrease Hover Verbosity Level"), + label: DECREASE_HOVER_VERBOSITY_ACTION_LABEL, alias: 'Decrease Hover Verbosity Level', - precondition: EditorContextKeys.hoverFocused + precondition: EditorContextKeys.hoverVisible }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { - HoverController.get(editor)?.updateFocusedMarkdownHoverVerbosityLevel(HoverVerbosityAction.Decrease); + public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void { + const hoverController = HoverController.get(editor); + if (!hoverController) { + return; + } + const index = args?.index !== undefined ? args.index : hoverController.focusedHoverPartIndex(); + HoverController.get(editor)?.updateHoverVerbosityLevel(HoverVerbosityAction.Decrease, index, args?.focus); } } diff --git a/src/vs/editor/contrib/hover/browser/hoverContribution.ts b/src/vs/editor/contrib/hover/browser/hoverContribution.ts index 629b189f2db..bf24cdc1c69 100644 --- a/src/vs/editor/contrib/hover/browser/hoverContribution.ts +++ b/src/vs/editor/contrib/hover/browser/hoverContribution.ts @@ -13,7 +13,7 @@ import { MarkerHoverParticipant } from 'vs/editor/contrib/hover/browser/markerHo import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import 'vs/css!./hover'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; -import { ExtHoverAccessibleView, HoverAccessibleView } from 'vs/editor/contrib/hover/browser/hoverAccessibleViews'; +import { ExtHoverAccessibleView, HoverAccessibilityHelp, HoverAccessibleView } from 'vs/editor/contrib/hover/browser/hoverAccessibleViews'; registerEditorContribution(HoverController.ID, HoverController, EditorContributionInstantiation.BeforeFirstInteraction); registerEditorAction(ShowOrFocusHoverAction); @@ -41,4 +41,5 @@ registerThemingParticipant((theme, collector) => { } }); AccessibleViewRegistry.register(new HoverAccessibleView()); +AccessibleViewRegistry.register(new HoverAccessibilityHelp()); AccessibleViewRegistry.register(new ExtHoverAccessibleView()); diff --git a/src/vs/editor/contrib/hover/browser/hoverController.ts b/src/vs/editor/contrib/hover/browser/hoverController.ts index 5d71cd64144..b6ef34860ca 100644 --- a/src/vs/editor/contrib/hover/browser/hoverController.ts +++ b/src/vs/editor/contrib/hover/browser/hoverController.ts @@ -23,6 +23,7 @@ import { ContentHoverWidget } from 'vs/editor/contrib/hover/browser/contentHover import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController'; import 'vs/css!./hover'; import { MarginHoverWidget } from 'vs/editor/contrib/hover/browser/marginHoverWidget'; +import { Emitter } from 'vs/base/common/event'; // sticky hover widget which doesn't disappear on focus out and such const _sticky = false @@ -47,8 +48,13 @@ const enum HoverWidgetType { export class HoverController extends Disposable implements IEditorContribution { + private readonly _onHoverContentsChanged = this._register(new Emitter()); + public readonly onHoverContentsChanged = this._onHoverContentsChanged.event; + public static readonly ID = 'editor.contrib.hover'; + public shouldKeepOpenOnEditorMouseMoveOrLeave: boolean = false; + private readonly _listenersStore = new DisposableStore(); private _glyphWidget: MarginHoverWidget | undefined; @@ -174,6 +180,9 @@ export class HoverController extends Disposable implements IEditorContribution { } private _onEditorMouseLeave(mouseEvent: IPartialEditorMouseEvent): void { + if (this.shouldKeepOpenOnEditorMouseMoveOrLeave) { + return; + } this._cancelScheduler(); @@ -223,6 +232,9 @@ export class HoverController extends Disposable implements IEditorContribution { } private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { + if (this.shouldKeepOpenOnEditorMouseMoveOrLeave) { + return; + } this._mouseMoveEvent = mouseEvent; if (this._contentWidget?.isFocused || this._contentWidget?.isResizing) { @@ -374,6 +386,7 @@ export class HoverController extends Disposable implements IEditorContribution { private _getOrCreateContentWidget(): ContentHoverController { if (!this._contentWidget) { this._contentWidget = this._instantiationService.createInstance(ContentHoverController, this._editor); + this._listenersStore.add(this._contentWidget.onContentsChanged(() => this._onHoverContentsChanged.fire())); } return this._contentWidget; } @@ -404,14 +417,26 @@ export class HoverController extends Disposable implements IEditorContribution { return this._contentWidget?.widget.isResizing || false; } - public updateFocusedMarkdownHoverVerbosityLevel(action: HoverVerbosityAction): void { - this._getOrCreateContentWidget().updateFocusedMarkdownHoverVerbosityLevel(action); + public focusedHoverPartIndex(): number { + return this._getOrCreateContentWidget().focusedHoverPartIndex(); + } + + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._getOrCreateContentWidget().doesHoverAtIndexSupportVerbosityAction(index, action); + } + + public updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): void { + this._getOrCreateContentWidget().updateHoverVerbosityLevel(action, index, focus); } public focus(): void { this._contentWidget?.focus(); } + public focusHoverPartWithIndex(index: number): void { + this._contentWidget?.focusHoverPartWithIndex(index); + } + public scrollUp(): void { this._contentWidget?.scrollUp(); } @@ -448,6 +473,14 @@ export class HoverController extends Disposable implements IEditorContribution { return this._contentWidget?.getWidgetContent(); } + public getAccessibleWidgetContent(): string | undefined { + return this._contentWidget?.getAccessibleWidgetContent(); + } + + public getAccessibleWidgetContentAtIndex(index: number): string | undefined { + return this._contentWidget?.getAccessibleWidgetContentAtIndex(index); + } + public get isColorPickerVisible(): boolean | undefined { return this._contentWidget?.isColorPickerVisible; } diff --git a/src/vs/editor/contrib/hover/browser/hoverTypes.ts b/src/vs/editor/contrib/hover/browser/hoverTypes.ts index 68483d2bbfa..4b23a838478 100644 --- a/src/vs/editor/contrib/hover/browser/hoverTypes.ts +++ b/src/vs/editor/contrib/hover/browser/hoverTypes.ts @@ -94,19 +94,7 @@ export interface IEditorHoverColorPickerWidget { layout(): void; } -export interface IEditorHoverRenderContext { - /** - * The fragment where dom elements should be attached. - */ - readonly fragment: DocumentFragment; - /** - * The status bar for actions for this hover. - */ - readonly statusBar: IEditorHoverStatusBar; - /** - * Set if the hover will render a color picker widget. - */ - setColorPicker(widget: IEditorHoverColorPickerWidget): void; +export interface IEditorHoverContext { /** * The contents rendered inside the fragment have been changed, which means that the hover should relayout. */ @@ -121,13 +109,58 @@ export interface IEditorHoverRenderContext { hide(): void; } +export interface IEditorHoverRenderContext extends IEditorHoverContext { + /** + * The fragment where dom elements should be attached. + */ + readonly fragment: DocumentFragment; + /** + * The status bar for actions for this hover. + */ + readonly statusBar: IEditorHoverStatusBar; +} + +export interface IRenderedHoverPart extends IDisposable { + /** + * The rendered hover part. + */ + hoverPart: T; + /** + * The HTML element containing the hover part. + */ + hoverElement: HTMLElement; +} + +export interface IRenderedHoverParts extends IDisposable { + /** + * Array of rendered hover parts. + */ + renderedHoverParts: IRenderedHoverPart[]; +} + +/** + * Default implementation of IRenderedHoverParts. + */ +export class RenderedHoverParts implements IRenderedHoverParts { + + constructor(public readonly renderedHoverParts: IRenderedHoverPart[]) { } + + dispose() { + for (const part of this.renderedHoverParts) { + part.dispose(); + } + } +} + export interface IEditorHoverParticipant { readonly hoverOrdinal: number; suggestHoverAnchor?(mouseEvent: IEditorMouseEvent): HoverAnchor | null; computeSync(anchor: HoverAnchor, lineDecorations: IModelDecoration[]): T[]; computeAsync?(anchor: HoverAnchor, lineDecorations: IModelDecoration[], token: CancellationToken): AsyncIterableObject; createLoadingMessage?(anchor: HoverAnchor): T | null; - renderHoverParts(context: IEditorHoverRenderContext, hoverParts: T[]): IDisposable; + renderHoverParts(context: IEditorHoverRenderContext, hoverParts: T[]): IRenderedHoverParts; + getAccessibleContent(hoverPart: T): string; + handleResize?(): void; } export type IEditorHoverParticipantCtor = IConstructorSignature; diff --git a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts index 3c11bee0849..aaeb5796aa6 100644 --- a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { asArray, compareBy, numberComparator } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IMarkdownString, isEmptyMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -15,7 +15,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IModelDecoration, ITextModel } from 'vs/editor/common/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { HoverAnchor, HoverAnchorType, HoverRangeAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverRangeAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -33,6 +33,7 @@ import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser import { AsyncIterableObject } from 'vs/base/common/async'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import { getHoverProviderResultsAsAsyncIterable } from 'vs/editor/contrib/hover/browser/getHover'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const $ = dom.$; const increaseHoverVerbosityIcon = registerIcon('hover-increase-verbosity', Codicon.add, nls.localize('increaseHoverVerbosity', 'Icon for increaseing hover verbosity.')); @@ -90,6 +91,7 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant { this._renderedHoverParts = new MarkdownRenderedHoverParts( hoverParts, context.fragment, + this, this._editor, this._languageService, this._openerService, + this._commandService, this._keybindingService, this._hoverService, this._configurationService, @@ -191,49 +195,65 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant { + return Promise.resolve(this._renderedHoverParts?.updateMarkdownHoverPartVerbosityLevel(action, index, focus)); } } -interface RenderedHoverPart { - renderedMarkdown: HTMLElement; - disposables: DisposableStore; - hoverSource?: HoverSource; -} - -interface FocusedHoverInfo { - hoverPartIndex: number; - // TODO@aiday-mar is this needed? - focusRemains: boolean; -} - -class MarkdownRenderedHoverParts extends Disposable { - - private _renderedHoverParts: RenderedHoverPart[]; - private _hoverFocusInfo: FocusedHoverInfo = { hoverPartIndex: -1, focusRemains: false }; - private _ongoingHoverOperations: Map = new Map(); +class RenderedMarkdownHoverPart implements IRenderedHoverPart { constructor( - hoverParts: MarkdownHover[], // we own! + public readonly hoverPart: MarkdownHover, + public readonly hoverElement: HTMLElement, + public readonly disposables: DisposableStore, + ) { } + + get hoverAccessibleContent(): string { + return this.hoverElement.innerText.trim(); + } + + dispose(): void { + this.disposables.dispose(); + } +} + +class MarkdownRenderedHoverParts implements IRenderedHoverParts { + + public renderedHoverParts: RenderedMarkdownHoverPart[]; + + private _ongoingHoverOperations: Map = new Map(); + + private readonly _disposables = new DisposableStore(); + + constructor( + hoverParts: MarkdownHover[], hoverPartsContainer: DocumentFragment, + private readonly _hoverParticipant: MarkdownHoverParticipant, private readonly _editor: ICodeEditor, private readonly _languageService: ILanguageService, private readonly _openerService: IOpenerService, + private readonly _commandService: ICommandService, private readonly _keybindingService: IKeybindingService, private readonly _hoverService: IHoverService, private readonly _configurationService: IConfigurationService, private readonly _onFinishedRendering: () => void, ) { - super(); - this._renderedHoverParts = this._renderHoverParts(hoverParts, hoverPartsContainer, this._onFinishedRendering); - this._register(toDisposable(() => { - this._renderedHoverParts.forEach(renderedHoverPart => { - renderedHoverPart.disposables.dispose(); + this.renderedHoverParts = this._renderHoverParts(hoverParts, hoverPartsContainer, this._onFinishedRendering); + this._disposables.add(toDisposable(() => { + this.renderedHoverParts.forEach(renderedHoverPart => { + renderedHoverPart.dispose(); + }); + this._ongoingHoverOperations.forEach(operation => { + operation.tokenSource.dispose(true); }); - })); - this._register(toDisposable(() => { - this._ongoingHoverOperations.forEach(operation => { operation.tokenSource.dispose(true); }); })); } @@ -241,80 +261,57 @@ class MarkdownRenderedHoverParts extends Disposable { hoverParts: MarkdownHover[], hoverPartsContainer: DocumentFragment, onFinishedRendering: () => void, - ): RenderedHoverPart[] { + ): RenderedMarkdownHoverPart[] { hoverParts.sort(compareBy(hover => hover.ordinal, numberComparator)); - return hoverParts.map((hoverPart, hoverIndex) => { - const renderedHoverPart = this._renderHoverPart( - hoverIndex, - hoverPart.contents, - hoverPart.source, - onFinishedRendering - ); - hoverPartsContainer.appendChild(renderedHoverPart.renderedMarkdown); + return hoverParts.map(hoverPart => { + const renderedHoverPart = this._renderHoverPart(hoverPart, onFinishedRendering); + hoverPartsContainer.appendChild(renderedHoverPart.hoverElement); return renderedHoverPart; }); } private _renderHoverPart( - hoverPartIndex: number, - hoverContents: IMarkdownString[], - hoverSource: HoverSource | undefined, + hoverPart: MarkdownHover, onFinishedRendering: () => void - ): RenderedHoverPart { + ): RenderedMarkdownHoverPart { - const { renderedMarkdown, disposables } = this._renderMarkdownContent(hoverContents, onFinishedRendering); + const renderedMarkdownPart = this._renderMarkdownHover(hoverPart, onFinishedRendering); + const renderedMarkdownElement = renderedMarkdownPart.hoverElement; + const hoverSource = hoverPart.source; + const disposables = new DisposableStore(); + disposables.add(renderedMarkdownPart); if (!hoverSource) { - return { renderedMarkdown, disposables }; + return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables); } const canIncreaseVerbosity = hoverSource.supportsVerbosityAction(HoverVerbosityAction.Increase); const canDecreaseVerbosity = hoverSource.supportsVerbosityAction(HoverVerbosityAction.Decrease); if (!canIncreaseVerbosity && !canDecreaseVerbosity) { - return { renderedMarkdown, disposables, hoverSource }; + return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables); } const actionsContainer = $('div.verbosity-actions'); - renderedMarkdown.prepend(actionsContainer); + renderedMarkdownElement.prepend(actionsContainer); disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Increase, canIncreaseVerbosity)); disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Decrease, canDecreaseVerbosity)); - - const focusTracker = disposables.add(dom.trackFocus(renderedMarkdown)); - disposables.add(focusTracker.onDidFocus(() => { - this._hoverFocusInfo = { - hoverPartIndex, - focusRemains: true - }; - })); - disposables.add(focusTracker.onDidBlur(() => { - if (this._hoverFocusInfo?.focusRemains) { - this._hoverFocusInfo.focusRemains = false; - return; - } - })); - return { renderedMarkdown, disposables, hoverSource }; + return new RenderedMarkdownHoverPart(hoverPart, renderedMarkdownElement, disposables); } - private _renderMarkdownContent( - markdownContent: IMarkdownString[], + private _renderMarkdownHover( + markdownHover: MarkdownHover, onFinishedRendering: () => void - ): RenderedHoverPart { - const renderedMarkdown = $('div.hover-row'); - renderedMarkdown.tabIndex = 0; - const renderedMarkdownContents = $('div.hover-row-contents'); - renderedMarkdown.appendChild(renderedMarkdownContents); - const disposables = new DisposableStore(); - disposables.add(renderMarkdownInContainer( + ): IRenderedHoverPart { + const renderedMarkdownHover = renderMarkdownInContainer( this._editor, - renderedMarkdownContents, - markdownContent, + markdownHover, this._languageService, this._openerService, onFinishedRendering, - )); - return { renderedMarkdown, disposables }; + ); + return renderedMarkdownHover; } private _renderHoverExpansionAction(container: HTMLElement, action: HoverVerbosityAction, actionEnabled: boolean): DisposableStore { @@ -323,53 +320,77 @@ class MarkdownRenderedHoverParts extends Disposable { const actionElement = dom.append(container, $(ThemeIcon.asCSSSelector(isActionIncrease ? increaseHoverVerbosityIcon : decreaseHoverVerbosityIcon))); actionElement.tabIndex = 0; const hoverDelegate = new WorkbenchHoverDelegate('mouse', false, { target: container, position: { hoverPosition: HoverPosition.LEFT } }, this._configurationService, this._hoverService); - if (isActionIncrease) { - const kb = this._keybindingService.lookupKeybinding(INCREASE_HOVER_VERBOSITY_ACTION_ID); - store.add(this._hoverService.setupUpdatableHover(hoverDelegate, actionElement, kb ? - nls.localize('increaseVerbosityWithKb', "Increase Verbosity ({0})", kb.getLabel()) : - nls.localize('increaseVerbosity', "Increase Verbosity"))); - } else { - const kb = this._keybindingService.lookupKeybinding(DECREASE_HOVER_VERBOSITY_ACTION_ID); - store.add(this._hoverService.setupUpdatableHover(hoverDelegate, actionElement, kb ? - nls.localize('decreaseVerbosityWithKb', "Decrease Verbosity ({0})", kb.getLabel()) : - nls.localize('decreaseVerbosity', "Decrease Verbosity"))); - } + store.add(this._hoverService.setupManagedHover(hoverDelegate, actionElement, labelForHoverVerbosityAction(this._keybindingService, action))); if (!actionEnabled) { actionElement.classList.add('disabled'); return store; } actionElement.classList.add('enabled'); - const actionFunction = () => this.updateFocusedHoverPartVerbosityLevel(action); + const actionFunction = () => this._commandService.executeCommand(action === HoverVerbosityAction.Increase ? INCREASE_HOVER_VERBOSITY_ACTION_ID : DECREASE_HOVER_VERBOSITY_ACTION_ID); store.add(new ClickAction(actionElement, actionFunction)); store.add(new KeyDownAction(actionElement, actionFunction, [KeyCode.Enter, KeyCode.Space])); return store; } - public async updateFocusedHoverPartVerbosityLevel(action: HoverVerbosityAction): Promise { + public async updateMarkdownHoverPartVerbosityLevel(action: HoverVerbosityAction, index: number, focus: boolean = true): Promise<{ hoverPart: MarkdownHover; hoverElement: HTMLElement } | undefined> { const model = this._editor.getModel(); if (!model) { - return; + return undefined; } - const hoverFocusedPartIndex = this._hoverFocusInfo.hoverPartIndex; - const hoverRenderedPart = this._getRenderedHoverPartAtIndex(hoverFocusedPartIndex); - if (!hoverRenderedPart || !hoverRenderedPart.hoverSource?.supportsVerbosityAction(action)) { - return; + const hoverRenderedPart = this._getRenderedHoverPartAtIndex(index); + const hoverSource = hoverRenderedPart?.hoverPart.source; + if (!hoverRenderedPart || !hoverSource?.supportsVerbosityAction(action)) { + return undefined; } - const hoverSource = hoverRenderedPart.hoverSource; const newHover = await this._fetchHover(hoverSource, model, action); if (!newHover) { - return; + return undefined; } const newHoverSource = new HoverSource(newHover, hoverSource.hoverProvider, hoverSource.hoverPosition); - const newHoverRenderedPart = this._renderHoverPart( - hoverFocusedPartIndex, + const initialHoverPart = hoverRenderedPart.hoverPart; + const newHoverPart = new MarkdownHover( + this._hoverParticipant, + initialHoverPart.range, newHover.contents, - newHoverSource, + initialHoverPart.isBeforeContent, + initialHoverPart.ordinal, + newHoverSource + ); + const newHoverRenderedPart = this._renderHoverPart( + newHoverPart, this._onFinishedRendering ); - this._replaceRenderedHoverPartAtIndex(hoverFocusedPartIndex, newHoverRenderedPart); - this._focusOnHoverPartWithIndex(hoverFocusedPartIndex); - this._onFinishedRendering(); + this._replaceRenderedHoverPartAtIndex(index, newHoverRenderedPart, newHoverPart); + if (focus) { + this._focusOnHoverPartWithIndex(index); + } + return { + hoverPart: newHoverPart, + hoverElement: newHoverRenderedPart.hoverElement + }; + } + + public getAccessibleContent(hoverPart: MarkdownHover): string | undefined { + const renderedHoverPartIndex = this.renderedHoverParts.findIndex(renderedHoverPart => renderedHoverPart.hoverPart === hoverPart); + if (renderedHoverPartIndex === -1) { + return undefined; + } + const renderedHoverPart = this._getRenderedHoverPartAtIndex(renderedHoverPartIndex); + if (!renderedHoverPart) { + return undefined; + } + const hoverElementInnerText = renderedHoverPart.hoverElement.innerText; + const accessibleContent = hoverElementInnerText.replace(/[^\S\n\r]+/gu, ' '); + return accessibleContent; + } + + public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + const hoverRenderedPart = this._getRenderedHoverPartAtIndex(index); + const hoverSource = hoverRenderedPart?.hoverPart.source; + if (!hoverRenderedPart || !hoverSource?.supportsVerbosityAction(action)) { + return false; + } + return true; } private async _fetchHover(hoverSource: HoverSource, model: ITextModel, action: HoverVerbosityAction): Promise { @@ -394,75 +415,109 @@ class MarkdownRenderedHoverParts extends Disposable { return hover; } - private _replaceRenderedHoverPartAtIndex(index: number, renderedHoverPart: RenderedHoverPart): void { - if (index >= this._renderHoverParts.length || index < 0) { + private _replaceRenderedHoverPartAtIndex(index: number, renderedHoverPart: RenderedMarkdownHoverPart, hoverPart: MarkdownHover): void { + if (index >= this.renderedHoverParts.length || index < 0) { return; } - const currentRenderedHoverPart = this._renderedHoverParts[index]; - const currentRenderedMarkdown = currentRenderedHoverPart.renderedMarkdown; - currentRenderedMarkdown.replaceWith(renderedHoverPart.renderedMarkdown); - currentRenderedHoverPart.disposables.dispose(); - this._renderedHoverParts[index] = renderedHoverPart; + const currentRenderedHoverPart = this.renderedHoverParts[index]; + const currentRenderedMarkdown = currentRenderedHoverPart.hoverElement; + const renderedMarkdown = renderedHoverPart.hoverElement; + const renderedChildrenElements = Array.from(renderedMarkdown.children); + currentRenderedMarkdown.replaceChildren(...renderedChildrenElements); + const newRenderedHoverPart = new RenderedMarkdownHoverPart( + hoverPart, + currentRenderedMarkdown, + renderedHoverPart.disposables + ); + currentRenderedMarkdown.focus(); + currentRenderedHoverPart.dispose(); + this.renderedHoverParts[index] = newRenderedHoverPart; } private _focusOnHoverPartWithIndex(index: number): void { - this._renderedHoverParts[index].renderedMarkdown.focus(); - this._hoverFocusInfo.focusRemains = true; + this.renderedHoverParts[index].hoverElement.focus(); } - private _getRenderedHoverPartAtIndex(index: number): RenderedHoverPart | undefined { - return this._renderedHoverParts[index]; + private _getRenderedHoverPartAtIndex(index: number): RenderedMarkdownHoverPart | undefined { + return this.renderedHoverParts[index]; + } + + public dispose(): void { + this._disposables.dispose(); } } export function renderMarkdownHovers( context: IEditorHoverRenderContext, - hoverParts: MarkdownHover[], + markdownHovers: MarkdownHover[], editor: ICodeEditor, languageService: ILanguageService, openerService: IOpenerService, -): IDisposable { +): IRenderedHoverParts { // Sort hover parts to keep them stable since they might come in async, out-of-order - hoverParts.sort(compareBy(hover => hover.ordinal, numberComparator)); - - const disposables = new DisposableStore(); - for (const hoverPart of hoverParts) { - disposables.add(renderMarkdownInContainer( + markdownHovers.sort(compareBy(hover => hover.ordinal, numberComparator)); + const renderedHoverParts: IRenderedHoverPart[] = []; + for (const markdownHover of markdownHovers) { + renderedHoverParts.push(renderMarkdownInContainer( editor, - context.fragment, - hoverPart.contents, + markdownHover, languageService, openerService, context.onContentsChanged, )); } - return disposables; + return new RenderedHoverParts(renderedHoverParts); } function renderMarkdownInContainer( editor: ICodeEditor, - container: DocumentFragment | HTMLElement, - markdownStrings: IMarkdownString[], + markdownHover: MarkdownHover, languageService: ILanguageService, openerService: IOpenerService, onFinishedRendering: () => void, -): IDisposable { - const store = new DisposableStore(); - for (const contents of markdownStrings) { - if (isEmptyMarkdownString(contents)) { +): IRenderedHoverPart { + const disposables = new DisposableStore(); + const renderedMarkdown = $('div.hover-row'); + const renderedMarkdownContents = $('div.hover-row-contents'); + renderedMarkdown.appendChild(renderedMarkdownContents); + const markdownStrings = markdownHover.contents; + for (const markdownString of markdownStrings) { + if (isEmptyMarkdownString(markdownString)) { continue; } const markdownHoverElement = $('div.markdown-hover'); const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); - const renderer = store.add(new MarkdownRenderer({ editor }, languageService, openerService)); - store.add(renderer.onDidRenderAsync(() => { + const renderer = disposables.add(new MarkdownRenderer({ editor }, languageService, openerService)); + disposables.add(renderer.onDidRenderAsync(() => { hoverContentsElement.className = 'hover-contents code-hover-contents'; onFinishedRendering(); })); - const renderedContents = store.add(renderer.render(contents)); + const renderedContents = disposables.add(renderer.render(markdownString)); hoverContentsElement.appendChild(renderedContents.element); - container.appendChild(markdownHoverElement); + renderedMarkdownContents.appendChild(markdownHoverElement); + } + const renderedHoverPart: IRenderedHoverPart = { + hoverPart: markdownHover, + hoverElement: renderedMarkdown, + dispose() { disposables.dispose(); } + }; + return renderedHoverPart; +} + +export function labelForHoverVerbosityAction(keybindingService: IKeybindingService, action: HoverVerbosityAction): string { + switch (action) { + case HoverVerbosityAction.Increase: { + const kb = keybindingService.lookupKeybinding(INCREASE_HOVER_VERBOSITY_ACTION_ID); + return kb ? + nls.localize('increaseVerbosityWithKb', "Increase Hover Verbosity ({0})", kb.getLabel()) : + nls.localize('increaseVerbosity', "Increase Hover Verbosity"); + } + case HoverVerbosityAction.Decrease: { + const kb = keybindingService.lookupKeybinding(DECREASE_HOVER_VERBOSITY_ACTION_ID); + return kb ? + nls.localize('decreaseVerbosityWithKb', "Decrease Hover Verbosity ({0})", kb.getLabel()) : + nls.localize('decreaseVerbosity', "Decrease Hover Verbosity"); + } } - return store; } diff --git a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts index 3ed0b3fab14..86dba6a80e3 100644 --- a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/resources'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -20,7 +20,7 @@ import { getCodeActions, quickFixCommandId } from 'vs/editor/contrib/codeAction/ import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; import { CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/browser/gotoError'; -import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import * as nls from 'vs/nls'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; @@ -90,20 +90,29 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant { if (!hoverParts.length) { - return Disposable.None; + return new RenderedHoverParts([]); } const disposables = new DisposableStore(); - hoverParts.forEach(msg => context.fragment.appendChild(this.renderMarkerHover(msg, disposables))); + const renderedHoverParts: IRenderedHoverPart[] = []; + hoverParts.forEach(hoverPart => { + const renderedMarkerHover = this._renderMarkerHover(hoverPart); + context.fragment.appendChild(renderedMarkerHover.hoverElement); + renderedHoverParts.push(renderedMarkerHover); + }); const markerHoverForStatusbar = hoverParts.length === 1 ? hoverParts[0] : hoverParts.sort((a, b) => MarkerSeverity.compare(a.marker.severity, b.marker.severity))[0]; this.renderMarkerStatusbar(context, markerHoverForStatusbar, disposables); - return disposables; + return new RenderedHoverParts(renderedHoverParts); } - private renderMarkerHover(markerHover: MarkerHover, disposables: DisposableStore): HTMLElement { + public getAccessibleContent(hoverPart: MarkerHover): string { + return hoverPart.marker.message; + } + + private _renderMarkerHover(markerHover: MarkerHover): IRenderedHoverPart { + const disposables: DisposableStore = new DisposableStore(); const hoverElement = $('div.hover-row'); - hoverElement.tabIndex = 0; const markerElement = dom.append(hoverElement, $('div.marker.hover-contents')); const { source, message, code, relatedInformation } = markerHover.marker; @@ -166,7 +175,12 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant = { + hoverPart: markerHover, + hoverElement, + dispose: () => disposables.dispose() + }; + return renderedHoverPart; } private renderMarkerStatusbar(context: IEditorHoverRenderContext, markerHover: MarkerHover, disposables: DisposableStore): void { diff --git a/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts b/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts index b41a164ec86..65762f5905b 100644 --- a/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts +++ b/src/vs/editor/contrib/hover/test/browser/contentHover.test.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController'; +import { RenderedContentHover } from 'vs/editor/contrib/hover/browser/contentHoverRendered'; import { IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { TestCodeEditorInstantiationOptions, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; @@ -18,7 +18,7 @@ suite('Content Hover', () => { test('issue #151235: Gitlens hover shows up in the wrong place', () => { const text = 'just some text'; withTestCodeEditor(text, {}, (editor) => { - const actual = ContentHoverController.computeHoverRanges( + const actual = RenderedContentHover.computeHoverPositions( editor, new Range(5, 5, 5, 5), [{ range: new Range(4, 1, 5, 6) }] @@ -27,8 +27,7 @@ suite('Content Hover', () => { actual, { showAtPosition: new Position(5, 5), - showAtSecondaryPosition: new Position(5, 5), - highlightRange: new Range(4, 1, 5, 6) + showAtSecondaryPosition: new Position(5, 5) } ); }); @@ -38,7 +37,7 @@ suite('Content Hover', () => { const text = 'just some text'; const opts: TestCodeEditorInstantiationOptions = { wordWrap: 'wordWrapColumn', wordWrapColumn: 6 }; withTestCodeEditor(text, opts, (editor) => { - const actual = ContentHoverController.computeHoverRanges( + const actual = RenderedContentHover.computeHoverPositions( editor, new Range(1, 8, 1, 8), [{ range: new Range(1, 1, 1, 15) }] @@ -47,8 +46,7 @@ suite('Content Hover', () => { actual, { showAtPosition: new Position(1, 8), - showAtSecondaryPosition: new Position(1, 6), - highlightRange: new Range(1, 1, 1, 15) + showAtSecondaryPosition: new Position(1, 6) } ); }); diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index 9860a3b677e..84760fdb6f4 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -25,6 +25,8 @@ import * as nls from 'vs/nls'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { getGoodIndentForLine, getIndentMetadata } from 'vs/editor/common/languages/autoIndent'; import { getReindentEditOperations } from '../common/indentation'; +import { getStandardTokenTypeAtPosition } from 'vs/editor/common/tokens/lineTokens'; +import { Position } from 'vs/editor/common/core/position'; export class IndentationToSpacesAction extends EditorAction { public static readonly ID = 'editor.action.indentationToSpaces'; @@ -416,7 +418,13 @@ export class AutoIndentOnPaste implements IEditorContribution { if (!model) { return; } - + const containsOnlyWhitespace = this.rangeContainsOnlyWhitespaceCharacters(model, range); + if (containsOnlyWhitespace) { + return; + } + if (isStartOrEndInString(model, range)) { + return; + } if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber)) { return; } @@ -462,7 +470,7 @@ export class AutoIndentOnPaste implements IEditorContribution { range: new Range(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), text: newIndent }); - firstLineText = newIndent + firstLineText.substr(oldIndentation.length); + firstLineText = newIndent + firstLineText.substring(oldIndentation.length); } else { const indentMetadata = getIndentMetadata(model, startLineNumber, this._languageConfigurationService); @@ -542,6 +550,35 @@ export class AutoIndentOnPaste implements IEditorContribution { } } + private rangeContainsOnlyWhitespaceCharacters(model: ITextModel, range: Range): boolean { + const lineContainsOnlyWhitespace = (content: string): boolean => { + return content.trim().length === 0; + }; + let containsOnlyWhitespace: boolean = true; + if (range.startLineNumber === range.endLineNumber) { + const lineContent = model.getLineContent(range.startLineNumber); + const linePart = lineContent.substring(range.startColumn - 1, range.endColumn - 1); + containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart); + } else { + for (let i = range.startLineNumber; i <= range.endLineNumber; i++) { + const lineContent = model.getLineContent(i); + if (i === range.startLineNumber) { + const linePart = lineContent.substring(range.startColumn - 1); + containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart); + } else if (i === range.endLineNumber) { + const linePart = lineContent.substring(0, range.endColumn - 1); + containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart); + } else { + containsOnlyWhitespace = model.getLineFirstNonWhitespaceColumn(i) === 0; + } + if (!containsOnlyWhitespace) { + break; + } + } + } + return containsOnlyWhitespace; + } + private shouldIgnoreLine(model: ITextModel, lineNumber: number): boolean { model.tokenization.forceTokenization(lineNumber); const nonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); @@ -565,6 +602,14 @@ export class AutoIndentOnPaste implements IEditorContribution { } } +function isStartOrEndInString(model: ITextModel, range: Range): boolean { + const isPositionInString = (position: Position): boolean => { + const tokenType = getStandardTokenTypeAtPosition(model, position); + return tokenType === StandardTokenType.String; + }; + return isPositionInString(range.getStartPosition()) || isPositionInString(range.getEndPosition()); +} + function getIndentationEditOperations(model: ITextModel, builder: IEditOperationBuilder, tabSize: number, tabsToSpaces: boolean): void { if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { // Model is empty diff --git a/src/vs/editor/contrib/indentation/common/indentation.ts b/src/vs/editor/contrib/indentation/common/indentation.ts index 760a14919fb..599d8012e0f 100644 --- a/src/vs/editor/contrib/indentation/common/indentation.ts +++ b/src/vs/editor/contrib/indentation/common/indentation.ts @@ -8,30 +8,28 @@ import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { normalizeIndentation } from 'vs/editor/common/core/indentation'; import { Selection } from 'vs/editor/common/core/selection'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ProcessedIndentRulesSupport } from 'vs/editor/common/languages/supports/indentationLineProcessor'; import { ITextModel } from 'vs/editor/common/model'; -export function getReindentEditOperations(model: ITextModel, languageConfigurationService: ILanguageConfigurationService, startLineNumber: number, endLineNumber: number, inheritedIndent?: string): ISingleEditOperation[] { +export function getReindentEditOperations(model: ITextModel, languageConfigurationService: ILanguageConfigurationService, startLineNumber: number, endLineNumber: number): ISingleEditOperation[] { if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { // Model is empty return []; } - const indentationRules = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentationRules; - if (!indentationRules) { + const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentRulesSupport; + if (!indentationRulesSupport) { return []; } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentationRulesSupport, languageConfigurationService); endLineNumber = Math.min(endLineNumber, model.getLineCount()); // Skip `unIndentedLinePattern` lines while (startLineNumber <= endLineNumber) { - if (!indentationRules.unIndentedLinePattern) { - break; - } - - const text = model.getLineContent(startLineNumber); - if (!indentationRules.unIndentedLinePattern.test(text)) { + if (!processedIndentRulesSupport.shouldIgnore(startLineNumber)) { break; } @@ -54,37 +52,19 @@ export function getReindentEditOperations(model: ITextModel, languageConfigurati const indentEdits: ISingleEditOperation[] = []; // indentation being passed to lines below - let globalIndent: string; // Calculate indentation for the first line // If there is no passed-in indentation, we use the indentation of the first line as base. const currentLineText = model.getLineContent(startLineNumber); - let adjustedLineContent = currentLineText; - if (inheritedIndent !== undefined && inheritedIndent !== null) { - globalIndent = inheritedIndent; - const oldIndentation = strings.getLeadingWhitespace(currentLineText); - - adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); - if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { - globalIndent = unshiftIndent(globalIndent); - adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length); - - } - if (currentLineText !== adjustedLineContent) { - indentEdits.push(EditOperation.replaceMove(new Selection(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), normalizeIndentation(globalIndent, indentSize, insertSpaces))); - } - } else { - globalIndent = strings.getLeadingWhitespace(currentLineText); - } - + let globalIndent = strings.getLeadingWhitespace(currentLineText); // idealIndentForNextLine doesn't equal globalIndent when there is a line matching `indentNextLinePattern`. let idealIndentForNextLine: string = globalIndent; - if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { + if (processedIndentRulesSupport.shouldIncrease(startLineNumber)) { idealIndentForNextLine = shiftIndent(idealIndentForNextLine); globalIndent = shiftIndent(globalIndent); } - else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { + else if (processedIndentRulesSupport.shouldIndentNextLine(startLineNumber)) { idealIndentForNextLine = shiftIndent(idealIndentForNextLine); } @@ -92,11 +72,14 @@ export function getReindentEditOperations(model: ITextModel, languageConfigurati // Calculate indentation adjustment for all following lines for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + if (doesLineStartWithString(model, lineNumber)) { + continue; + } const text = model.getLineContent(lineNumber); const oldIndentation = strings.getLeadingWhitespace(text); - const adjustedLineContent = idealIndentForNextLine + text.substring(oldIndentation.length); + const currentIdealIndent = idealIndentForNextLine; - if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) { + if (processedIndentRulesSupport.shouldDecrease(lineNumber, currentIdealIndent)) { idealIndentForNextLine = unshiftIndent(idealIndentForNextLine); globalIndent = unshiftIndent(globalIndent); } @@ -106,14 +89,14 @@ export function getReindentEditOperations(model: ITextModel, languageConfigurati } // calculate idealIndentForNextLine - if (indentationRules.unIndentedLinePattern && indentationRules.unIndentedLinePattern.test(text)) { + if (processedIndentRulesSupport.shouldIgnore(lineNumber)) { // In reindent phase, if the line matches `unIndentedLinePattern` we inherit indentation from above lines // but don't change globalIndent and idealIndentForNextLine. continue; - } else if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) { + } else if (processedIndentRulesSupport.shouldIncrease(lineNumber, currentIdealIndent)) { globalIndent = shiftIndent(globalIndent); idealIndentForNextLine = globalIndent; - } else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) { + } else if (processedIndentRulesSupport.shouldIndentNextLine(lineNumber, currentIdealIndent)) { idealIndentForNextLine = shiftIndent(idealIndentForNextLine); } else { idealIndentForNextLine = globalIndent; @@ -122,3 +105,11 @@ export function getReindentEditOperations(model: ITextModel, languageConfigurati return indentEdits; } + +function doesLineStartWithString(model: ITextModel, lineNumber: number): boolean { + if (!model.tokenization.isCheapToTokenize(lineNumber)) { + return false; + } + const lineTokens = model.tokenization.getLineTokens(lineNumber); + return lineTokens.getStandardTokenType(0) === StandardTokenType.String; +} diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 72350c3ed69..2beed4f823b 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; @@ -22,9 +22,12 @@ import { goIndentationRules, htmlIndentationRules, javascriptIndentationRules, l import { cppOnEnterRules, htmlOnEnterRules, javascriptOnEnterRules, phpOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules'; import { TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations'; import { cppBracketRules, goBracketRules, htmlBracketRules, latexBracketRules, luaBracketRules, phpBracketRules, rubyBracketRules, typescriptBracketRules, vbBracketRules } from 'vs/editor/test/common/modes/supports/bracketRules'; -import { latexAutoClosingPairsRules } from 'vs/editor/test/common/modes/supports/autoClosingPairsRules'; +import { javascriptAutoClosingPairsRules, latexAutoClosingPairsRules } from 'vs/editor/test/common/modes/supports/autoClosingPairsRules'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; -enum Language { +export enum Language { TypeScript = 'ts-test', Ruby = 'ruby-test', PHP = 'php-test', @@ -44,16 +47,11 @@ function testIndentationToTabsCommand(lines: string[], selection: Selection, tab testCommand(lines, null, selection, (accessor, sel) => new IndentationToTabsCommand(sel, tabSize), expectedLines, expectedSelection); } -function registerLanguage(instantiationService: TestInstantiationService, language: Language): IDisposable { - const disposables = new DisposableStore(); - const languageService = instantiationService.get(ILanguageService); - disposables.add(registerLanguageConfiguration(instantiationService, language)); - disposables.add(languageService.registerLanguage({ id: language })); - return disposables; +export function registerLanguage(languageService: ILanguageService, language: Language): IDisposable { + return languageService.registerLanguage({ id: language }); } -function registerLanguageConfiguration(instantiationService: TestInstantiationService, language: Language): IDisposable { - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); +export function registerLanguageConfiguration(languageConfigurationService: ILanguageConfigurationService, language: Language): IDisposable { switch (language) { case Language.TypeScript: return languageConfigurationService.register(language, { @@ -62,6 +60,7 @@ function registerLanguageConfiguration(instantiationService: TestInstantiationSe lineComment: '//', blockComment: ['/*', '*/'] }, + autoClosingPairs: javascriptAutoClosingPairsRules, indentationRules: javascriptIndentationRules, onEnterRules: javascriptOnEnterRules }); @@ -110,12 +109,12 @@ function registerLanguageConfiguration(instantiationService: TestInstantiationSe } } -interface StandardTokenTypeData { +export interface StandardTokenTypeData { startIndex: number; standardTokenType: StandardTokenType; } -function registerTokenizationSupport(instantiationService: TestInstantiationService, tokens: StandardTokenTypeData[][], languageId: string): IDisposable { +export function registerTokenizationSupport(instantiationService: TestInstantiationService, tokens: StandardTokenTypeData[][], languageId: Language): IDisposable { let lineIndex = 0; const languageService = instantiationService.get(ILanguageService); const tokenizationSupport: ITokenizationSupport = { @@ -317,9 +316,20 @@ suite('Indent With Tab - TypeScript/JavaScript', () => { const languageId = Language.TypeScript; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -343,9 +353,7 @@ suite('Indent With Tab - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(1, 1, 3, 5)); editor.executeCommands('editor.action.indentLines', TypeOperations.indent(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); assert.strictEqual(model.getValue(), [ @@ -369,9 +377,7 @@ suite('Indent With Tab - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(1, 1, 5, 2)); editor.executeCommands('editor.action.indentLines', TypeOperations.indent(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); assert.strictEqual(model.getValue(), [ @@ -389,9 +395,20 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const languageId = Language.TypeScript; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -405,7 +422,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const pasteText = [ '/**', ' * JSDoc', @@ -439,7 +456,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { { startIndex: 15, standardTokenType: StandardTokenType.Other }, ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(pasteText, true, undefined, 'keyboard'); @@ -453,7 +469,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { // no need for tokenization because there are no comments const pasteText = [ @@ -470,7 +486,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { '}' ].join('\n'); - disposables.add(registerLanguage(instantiationService, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(pasteText, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(1, 1, 11, 2)); @@ -488,8 +503,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 6, 2, 6)); const text = ', null'; viewModel.paste(text, true, undefined, 'keyboard'); @@ -516,8 +530,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(5, 24, 5, 34)); const text = 'IMacLinuxKeyMapping'; viewModel.paste(text, true, undefined, 'keyboard'); @@ -541,8 +554,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel('', languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const text = [ '/*----------------', ' * Copyright (c) ', @@ -556,6 +568,42 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { }); }); + test('issue #209859: do not do change indentation when pasted inside of a string', () => { + + // issue: https://github.com/microsoft/vscode/issues/209859 + // issue: https://github.com/microsoft/vscode/issues/209418 + + const initialText = [ + 'const foo = "some text', + ' which is strangely', + ' indented"' + ].join('\n'); + const model = createTextModel(initialText, languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 12, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.String }, + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + + editor.setSelection(new Selection(2, 10, 2, 15)); + viewModel.paste('which', true, undefined, 'keyboard'); + const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); + autoIndentOnPasteController.trigger(new Range(2, 1, 2, 28)); + assert.strictEqual(model.getValue(), initialText); + }); + }); + // Failing tests found in issues... test.skip('issue #181065: Incorrect paste of object within comment', () => { @@ -565,7 +613,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const text = [ '/**', ' * @typedef {', @@ -597,7 +645,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { { startIndex: 3, standardTokenType: StandardTokenType.Other }, ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); @@ -617,7 +664,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 1, 2, 1)); const text = [ '() => {', @@ -625,7 +672,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { '}', '' ].join('\n'); - disposables.add(registerLanguage(instantiationService, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(2, 1, 5, 1)); @@ -659,7 +705,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 5, 2, 5)); const text = [ '() => {', @@ -667,7 +713,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { '}', ' ' ].join('\n'); - disposables.add(registerLanguage(instantiationService, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); // todo@aiday-mar, make sure range is correct, and make test work as in real life @@ -690,7 +735,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel('', languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(2, 5, 2, 5)); const text = [ 'function makeSub(a,b) {', @@ -698,7 +743,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { 'return subsent;', '}', ].join('\n'); - disposables.add(registerLanguage(instantiationService, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); // todo@aiday-mar, make sure range is correct, and make test work as in real life @@ -723,7 +767,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel, instantiationService) => { + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel, instantiationService) => { const tokens: StandardTokenTypeData[][] = [ [ { startIndex: 0, standardTokenType: StandardTokenType.Other }, @@ -754,7 +798,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { { startIndex: 0, standardTokenType: StandardTokenType.Other }, { startIndex: 1, standardTokenType: StandardTokenType.Other }] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); editor.setSelection(new Selection(2, 1, 2, 1)); @@ -762,7 +805,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { '// comment', 'const foo = 42', ].join('\n'); - disposables.add(registerLanguage(instantiationService, languageId)); const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); viewModel.paste(text, true, undefined, 'keyboard'); autoIndentOnPasteController.trigger(new Range(2, 1, 3, 15)); @@ -780,9 +822,20 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { const languageId = Language.TypeScript; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -800,8 +853,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type('const add1 = (n) =>'); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -822,8 +874,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 9, 3, 9)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -842,10 +893,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type([ 'const add1 = (n) =>', ' n + 1;', @@ -871,9 +919,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 1, 3, 1)); viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -902,8 +948,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: 'advanced', serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(7, 6, 7, 6)); viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(), @@ -933,8 +978,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: 'advanced', serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 4, 1, 4)); viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(), @@ -959,8 +1003,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 12, 2, 12)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -983,9 +1026,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 19, 2, 19)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1007,9 +1048,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { }); }); - // Failing tests... - - test.skip('issue #208232: incorrect indentation inside of comments', () => { + test('issue #208232: incorrect indentation inside of comments', () => { // https://github.com/microsoft/vscode/issues/208232 @@ -1020,9 +1059,13 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }], + [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }], + [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); editor.setSelection(new Selection(2, 23, 2, 23)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1034,6 +1077,40 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { }); }); + test('issue #209802: allman style braces in JavaScript', () => { + + // https://github.com/microsoft/vscode/issues/209802 + + const model = createTextModel([ + 'if (/*condition*/)', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(1, 19, 1, 19)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (/*condition*/)', + ' ' + ].join('\n')); + viewModel.type("{", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (/*condition*/)', + '{}' + ].join('\n')); + editor.setSelection(new Selection(2, 2, 2, 2)); + viewModel.type("\n", 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'if (/*condition*/)', + '{', + ' ', + '}' + ].join('\n')); + }); + }); + + // Failing tests... + test.skip('issue #43244: indent after equal sign is detected', () => { // https://github.com/microsoft/vscode/issues/43244 @@ -1047,8 +1124,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 14, 1, 14)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1071,8 +1147,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 7, 2, 7)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1095,8 +1170,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 7, 2, 7)); viewModel.type("\n", 'keyboard'); viewModel.type("."); @@ -1120,8 +1194,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 24, 2, 24)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1146,8 +1219,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 5, 3, 5)); viewModel.type("."); assert.strictEqual(model.getValue(), [ @@ -1170,8 +1242,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 25, 2, 25)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1190,10 +1261,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { const model = createTextModel('function foo() {}', languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 17, 1, 17)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1224,9 +1292,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 14, 3, 14)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1252,9 +1318,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(4, 1, 4, 1)); viewModel.type("}", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1276,16 +1340,13 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 5, 2, 5)); viewModel.type("{}", 'keyboard'); assert.strictEqual(model.getValue(), [ 'if (true)', '{}', ].join('\n')); - editor.setSelection(new Selection(2, 2, 2, 2)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1307,8 +1368,7 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "keep" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "keep", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 5, 2, 5)); viewModel.type("}", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1323,9 +1383,20 @@ suite('Auto Indent On Type - Ruby', () => { const languageId = Language.Ruby; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1341,10 +1412,7 @@ suite('Auto Indent On Type - Ruby', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type("def foo\n i"); viewModel.type("n", 'keyboard'); assert.strictEqual(model.getValue(), "def foo\n in"); @@ -1369,10 +1437,7 @@ suite('Auto Indent On Type - Ruby', () => { const model = createTextModel("", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); - + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { viewModel.type("method('#foo') do"); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1387,9 +1452,20 @@ suite('Auto Indent On Type - PHP', () => { const languageId = Language.PHP; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1398,24 +1474,26 @@ suite('Auto Indent On Type - PHP', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('temp issue because there should be at least one passing test in a suite', () => { - assert.ok(true); - }); - - test.skip('issue #199050: should not indent after { detected in a string', () => { + test('issue #199050: should not indent after { detected in a string', () => { // https://github.com/microsoft/vscode/issues/199050 - const model = createTextModel("$phrase = preg_replace('#(\{1|%s).*#su', '', $phrase);", languageId, {}); + const model = createTextModel("preg_replace('{');", languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 13, standardTokenType: StandardTokenType.String }, + { startIndex: 16, standardTokenType: StandardTokenType.Other }, + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); editor.setSelection(new Selection(1, 54, 1, 54)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ - "$phrase = preg_replace('#(\{1|%s).*#su', '', $phrase);", + "preg_replace('{');", "" ].join('\n')); }); @@ -1426,9 +1504,20 @@ suite('Auto Indent On Paste - Go', () => { const languageId = Language.Go; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1453,8 +1542,7 @@ suite('Auto Indent On Paste - Go', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(3, 1, 3, 1)); const text = ' '; const autoIndentOnPasteController = editor.registerAndInstantiateContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste); @@ -1474,9 +1562,20 @@ suite('Auto Indent On Type - CPP', () => { const languageId = Language.CPP; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1499,8 +1598,7 @@ suite('Auto Indent On Type - CPP', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 20, 2, 20)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1522,8 +1620,7 @@ suite('Auto Indent On Type - CPP', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 20, 1, 20)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1544,9 +1641,7 @@ suite('Auto Indent On Type - CPP', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "none" }, (editor, viewModel, instantiationService) => { - - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "none", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 3, 2, 3)); viewModel.type("}", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1562,9 +1657,20 @@ suite('Auto Indent On Type - HTML', () => { const languageId = Language.HTML; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1588,8 +1694,7 @@ suite('Auto Indent On Type - HTML', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 48, 2, 48)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1606,9 +1711,20 @@ suite('Auto Indent On Type - Visual Basic', () => { const languageId = Language.VB; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1632,8 +1748,7 @@ suite('Auto Indent On Type - Visual Basic', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { editor.setSelection(new Selection(3, 10, 3, 10)); viewModel.type("f", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1650,9 +1765,20 @@ suite('Auto Indent On Type - Latex', () => { const languageId = Language.Latex; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1675,8 +1801,7 @@ suite('Auto Indent On Type - Latex', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(2, 9, 2, 9)); viewModel.type("{", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1691,9 +1816,20 @@ suite('Auto Indent On Type - Lua', () => { const languageId = Language.Lua; let disposables: DisposableStore; + let serviceCollection: ServiceCollection; setup(() => { disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); }); teardown(() => { @@ -1715,8 +1851,7 @@ suite('Auto Indent On Type - Lua', () => { ].join('\n'), languageId, {}); disposables.add(model); - withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => { - disposables.add(registerLanguage(instantiationService, languageId)); + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => { editor.setSelection(new Selection(1, 28, 1, 28)); viewModel.type("\n", 'keyboard'); assert.strictEqual(model.getValue(), [ @@ -1726,4 +1861,3 @@ suite('Auto Indent On Type - Lua', () => { }); }); }); - diff --git a/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts new file mode 100644 index 00000000000..2004b864cbe --- /dev/null +++ b/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts @@ -0,0 +1,256 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IndentationContextProcessor, ProcessedIndentRulesSupport } from 'vs/editor/common/languages/supports/indentationLineProcessor'; +import { Language, registerLanguage, registerLanguageConfiguration, registerTokenizationSupport, StandardTokenTypeData } from 'vs/editor/contrib/indentation/test/browser/indentation.test'; +import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { Range } from 'vs/editor/common/core/range'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { ILanguageService } from 'vs/editor/common/languages/language'; + +suite('Indentation Context Processor - TypeScript/JavaScript', () => { + + const languageId = Language.TypeScript; + let disposables: DisposableStore; + let serviceCollection: ServiceCollection; + + setup(() => { + disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('brackets inside of string', () => { + + const model = createTextModel([ + 'const someVar = "{some text}"', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [[ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 16, standardTokenType: StandardTokenType.String }, + { startIndex: 28, standardTokenType: StandardTokenType.String } + ]]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContext = indentationContextProcessor.getProcessedTokenContextAroundRange(new Range(1, 23, 1, 23)); + assert.strictEqual(processedContext.beforeRangeProcessedTokens.getLineContent(), 'const someVar = "some'); + assert.strictEqual(processedContext.afterRangeProcessedTokens.getLineContent(), ' text"'); + assert.strictEqual(processedContext.previousLineProcessedTokens.getLineContent(), ''); + }); + }); + + test('brackets inside of comment', () => { + + const model = createTextModel([ + 'const someVar2 = /*(a])*/', + 'const someVar = /* [()] some other t{e}xt() */ "some text"', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 17, standardTokenType: StandardTokenType.Comment }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 16, standardTokenType: StandardTokenType.Comment }, + { startIndex: 46, standardTokenType: StandardTokenType.Other }, + { startIndex: 47, standardTokenType: StandardTokenType.String } + ]]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContext = indentationContextProcessor.getProcessedTokenContextAroundRange(new Range(2, 29, 2, 35)); + assert.strictEqual(processedContext.beforeRangeProcessedTokens.getLineContent(), 'const someVar = /* some'); + assert.strictEqual(processedContext.afterRangeProcessedTokens.getLineContent(), ' text */ "some text"'); + assert.strictEqual(processedContext.previousLineProcessedTokens.getLineContent(), 'const someVar2 = /*a*/'); + }); + }); + + test('brackets inside of regex', () => { + + const model = createTextModel([ + 'const someRegex2 = /(()))]/;', + 'const someRegex = /()a{h}{s}[(a}87(9a9()))]/;', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 19, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 27, standardTokenType: StandardTokenType.Other }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 18, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 44, standardTokenType: StandardTokenType.Other }, + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); + const processedContext = indentationContextProcessor.getProcessedTokenContextAroundRange(new Range(1, 25, 2, 33)); + assert.strictEqual(processedContext.beforeRangeProcessedTokens.getLineContent(), 'const someRegex2 = /'); + assert.strictEqual(processedContext.afterRangeProcessedTokens.getLineContent(), '879a9/;'); + assert.strictEqual(processedContext.previousLineProcessedTokens.getLineContent(), ''); + }); + }); +}); + +suite('Processed Indent Rules Support - TypeScript/JavaScript', () => { + + const languageId = Language.TypeScript; + let disposables: DisposableStore; + let serviceCollection: ServiceCollection; + + setup(() => { + disposables = new DisposableStore(); + const languageService = new LanguageService(); + const languageConfigurationService = new TestLanguageConfigurationService(); + disposables.add(languageService); + disposables.add(languageConfigurationService); + disposables.add(registerLanguage(languageService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + serviceCollection = new ServiceCollection( + [ILanguageService, languageService], + [ILanguageConfigurationService, languageConfigurationService] + ); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should increase', () => { + + const model = createTextModel([ + 'const someVar = {', + 'const someVar2 = "{"', + 'const someVar3 = /*{*/' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 17, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 17, standardTokenType: StandardTokenType.Comment }, + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; + if (!indentationRulesSupport) { + assert.fail('indentationRulesSupport should be defined'); + } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentationRulesSupport, languageConfigurationService); + assert.strictEqual(processedIndentRulesSupport.shouldIncrease(1), true); + assert.strictEqual(processedIndentRulesSupport.shouldIncrease(2), false); + assert.strictEqual(processedIndentRulesSupport.shouldIncrease(3), false); + }); + }); + + test('should decrease', () => { + + const model = createTextModel([ + '}', + '"])some text}"', + '])*/' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [{ startIndex: 0, standardTokenType: StandardTokenType.Other }], + [{ startIndex: 0, standardTokenType: StandardTokenType.String }], + [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; + if (!indentationRulesSupport) { + assert.fail('indentationRulesSupport should be defined'); + } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentationRulesSupport, languageConfigurationService); + assert.strictEqual(processedIndentRulesSupport.shouldDecrease(1), true); + assert.strictEqual(processedIndentRulesSupport.shouldDecrease(2), false); + assert.strictEqual(processedIndentRulesSupport.shouldDecrease(3), false); + }); + }); + + test('should increase next line', () => { + + const model = createTextModel([ + 'if()', + 'const someString = "if()"', + 'const someRegex = /if()/' + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel, instantiationService) => { + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 19, standardTokenType: StandardTokenType.String } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 18, standardTokenType: StandardTokenType.RegEx } + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; + if (!indentationRulesSupport) { + assert.fail('indentationRulesSupport should be defined'); + } + const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentationRulesSupport, languageConfigurationService); + assert.strictEqual(processedIndentRulesSupport.shouldIndentNextLine(1), true); + assert.strictEqual(processedIndentRulesSupport.shouldIndentNextLine(2), false); + assert.strictEqual(processedIndentRulesSupport.shouldIndentNextLine(3), false); + }); + }); +}); diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index b2b665a7edd..accb88043ff 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ModifierKeyEmitter } from 'vs/base/browser/dom'; +import { isHTMLElement, ModifierKeyEmitter } from 'vs/base/browser/dom'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -358,7 +358,7 @@ export class InlayHintsController implements IEditorContribution { private _installContextMenu(): IDisposable { return this._editor.onContextMenu(async e => { - if (!(e.event.target instanceof HTMLElement)) { + if (!(isHTMLElement(e.event.target))) { return; } const part = this._getInlayHintLabelPart(e); diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts index d49a4bb781f..05703e108d1 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts @@ -26,6 +26,7 @@ import { asCommandLink } from 'vs/editor/contrib/inlayHints/browser/inlayHints'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { ICommandService } from 'vs/platform/commands/common/commands'; class InlayHintsHoverAnchor extends HoverForeignElementAnchor { constructor( @@ -51,8 +52,9 @@ export class InlayHintsHover extends MarkdownHoverParticipant implements IEditor @IConfigurationService configurationService: IConfigurationService, @ITextModelService private readonly _resolverService: ITextModelService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @ICommandService commandService: ICommandService ) { - super(editor, languageService, openerService, configurationService, languageFeaturesService, keybindingService, hoverService); + super(editor, languageService, openerService, configurationService, languageFeaturesService, keybindingService, hoverService, commandService); } suggestHoverAnchor(mouseEvent: IEditorMouseEvent): HoverAnchor | null { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts index 0c93d8465ab..450f9549fb7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts @@ -34,7 +34,7 @@ export interface IGhostTextWidgetModel { export class GhostTextWidget extends Disposable { private readonly isDisposed = observableValue(this, false); - private readonly currentTextModel = observableFromEvent(this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); + private readonly currentTextModel = observableFromEvent(this, this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); constructor( private readonly editor: ICodeEditor, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts b/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts index 90d4ffcba0c..7bcf0ea59f1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/hoverParticipant.ts @@ -12,7 +12,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IModelDecoration } from 'vs/editor/common/model'; -import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget'; import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; @@ -94,8 +94,8 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan return []; } - renderHoverParts(context: IEditorHoverRenderContext, hoverParts: InlineCompletionsHover[]): IDisposable { - const disposableStore = new DisposableStore(); + renderHoverParts(context: IEditorHoverRenderContext, hoverParts: InlineCompletionsHover[]): IRenderedHoverParts { + const disposables = new DisposableStore(); const part = hoverParts[0]; this._telemetryService.publicLog2<{}, { @@ -104,7 +104,7 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan }>('inlineCompletionHover.shown'); if (this.accessibilityService.isScreenReaderOptimized() && !this._editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { - this.renderScreenReaderText(context, part, disposableStore); + disposables.add(this.renderScreenReaderText(context, part)); } const model = part.controller.model.get()!; @@ -115,32 +115,42 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan model.inlineCompletionsCount, model.activeCommands, ); - context.fragment.appendChild(w.getDomNode()); + const widgetNode: HTMLElement = w.getDomNode(); + context.fragment.appendChild(widgetNode); model.triggerExplicitly(); - disposableStore.add(w); - - return disposableStore; + disposables.add(w); + const renderedHoverPart: IRenderedHoverPart = { + hoverPart: part, + hoverElement: widgetNode, + dispose() { disposables.dispose(); } + }; + return new RenderedHoverParts([renderedHoverPart]); } - private renderScreenReaderText(context: IEditorHoverRenderContext, part: InlineCompletionsHover, disposableStore: DisposableStore) { + getAccessibleContent(hoverPart: InlineCompletionsHover): string { + return nls.localize('hoverAccessibilityStatusBar', 'There are inline completions here'); + } + + private renderScreenReaderText(context: IEditorHoverRenderContext, part: InlineCompletionsHover): IDisposable { + const disposables = new DisposableStore(); const $ = dom.$; const markdownHoverElement = $('div.hover-row.markdown-hover'); const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents', { ['aria-live']: 'assertive' })); - const renderer = disposableStore.add(new MarkdownRenderer({ editor: this._editor }, this._languageService, this._openerService)); + const renderer = disposables.add(new MarkdownRenderer({ editor: this._editor }, this._languageService, this._openerService)); const render = (code: string) => { - disposableStore.add(renderer.onDidRenderAsync(() => { + disposables.add(renderer.onDidRenderAsync(() => { hoverContentsElement.className = 'hover-contents code-hover-contents'; context.onContentsChanged(); })); const inlineSuggestionAvailable = nls.localize('inlineSuggestionFollows', "Suggestion:"); - const renderedContents = disposableStore.add(renderer.render(new MarkdownString().appendText(inlineSuggestionAvailable).appendCodeblock('text', code))); + const renderedContents = disposables.add(renderer.render(new MarkdownString().appendText(inlineSuggestionAvailable).appendCodeblock('text', code))); hoverContentsElement.replaceChildren(renderedContents.element); }; - disposableStore.add(autorun(reader => { + disposables.add(autorun(reader => { /** @description update hover */ const ghostText = part.controller.model.read(reader)?.primaryGhostText.read(reader); if (ghostText) { @@ -152,5 +162,6 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan })); context.fragment.appendChild(markdownHoverElement); + return disposables; } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts index c11920c4d65..a7c25e42677 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController.ts @@ -3,30 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createStyleSheet2 } from 'vs/base/browser/dom'; +import { createStyleSheetFromObservable } from 'vs/base/browser/domObservable'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { timeout } from 'vs/base/common/async'; import { cancelOnDispose } from 'vs/base/common/cancellation'; -import { itemEquals, itemsEquals } from 'vs/base/common/equals'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, ITransaction, autorun, autorunHandleChanges, constObservable, derived, disposableObservableValue, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from 'vs/base/common/observable'; -import { ISettableObservable, observableValueOpts } from 'vs/base/common/observableInternal/base'; -import { mapObservableArrayCached } from 'vs/base/common/observableInternal/utils'; +import { IObservable, ITransaction, autorun, constObservable, derived, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from 'vs/base/common/observable'; +import { ISettableObservable } from 'vs/base/common/observableInternal/base'; +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; +import { derivedObservableWithCache, mapObservableArrayCached } from 'vs/base/common/observableInternal/utils'; import { isUndefined } from 'vs/base/common/types'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor, reactToChange, reactToChangeWithStore } from 'vs/editor/browser/observableCodeEditor'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/commandIds'; import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget'; import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys'; import { InlineCompletionsHintsWidget, InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget'; -import { InlineCompletionsModel, VersionIdChangeReason } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; +import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; import { SuggestWidgetAdaptor } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -44,54 +44,77 @@ export class InlineCompletionsController extends Disposable { return editor.getContribution(InlineCompletionsController.ID); } - public readonly model = this._register(disposableObservableValue('inlineCompletionModel', undefined)); - private readonly _textModelVersionId = observableValue(this, -1); - private readonly _positions = observableValueOpts({ owner: this, equalsFn: itemsEquals(itemEquals()) }, [new Position(1, 1)]); + private readonly _editorObs = observableCodeEditor(this.editor); + private readonly _positions = derived(this, reader => this._editorObs.selections.read(reader)?.map(s => s.getEndPosition()) ?? [new Position(1, 1)]); + private readonly _suggestWidgetAdaptor = this._register(new SuggestWidgetAdaptor( this.editor, - () => this.model.get()?.selectedInlineCompletion.get()?.toSingleTextEdit(undefined), - (tx) => this.updateObservables(tx, VersionIdChangeReason.Other), - (item) => { - transaction(tx => { - /** @description InlineCompletionsController.handleSuggestAccepted */ - this.updateObservables(tx, VersionIdChangeReason.Other); - this.model.get()?.handleSuggestAccepted(item); - }); - } + () => { + this._editorObs.forceUpdate(); + return this.model.get()?.selectedInlineCompletion.get()?.toSingleTextEdit(undefined); + }, + (item) => this._editorObs.forceUpdate(_tx => { + /** @description InlineCompletionsController.handleSuggestAccepted */ + this.model.get()?.handleSuggestAccepted(item); + }) )); - private readonly _enabledInConfig = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled); - private readonly _isScreenReaderEnabled = observableFromEvent(this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); - private readonly _editorDictationInProgress = observableFromEvent(this._contextKeyService.onDidChangeContext, () => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true); + + private readonly _suggestWidgetSelectedItem = observableFromEvent(this, cb => this._suggestWidgetAdaptor.onDidSelectedItemChange(() => { + this._editorObs.forceUpdate(_tx => cb(undefined)); + }), () => this._suggestWidgetAdaptor.selectedItem); + + + private readonly _enabledInConfig = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled); + private readonly _isScreenReaderEnabled = observableFromEvent(this, this._accessibilityService.onDidChangeScreenReaderOptimized, () => this._accessibilityService.isScreenReaderOptimized()); + private readonly _editorDictationInProgress = observableFromEvent(this, + this._contextKeyService.onDidChangeContext, + () => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true + ); private readonly _enabled = derived(this, reader => this._enabledInConfig.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader))); - private readonly _fontFamily = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).fontFamily); - - private readonly _ghostTexts = derived(this, (reader) => { - const model = this.model.read(reader); - return model?.ghostTexts.read(reader) ?? []; - }); - - private readonly _stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); - - private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => { - return store.add(this._instantiationService.createInstance(GhostTextWidget, this.editor, { - ghostText: ghostText, - minReservedLineCount: constObservable(0), - targetTextModel: this.model.map(v => v?.textModel), - })); - }).recomputeInitiallyAndOnChange(this._store); - private readonly _debounceValue = this._debounceService.for( this._languageFeaturesService.inlineCompletionsProvider, 'InlineCompletionsDebounce', { min: 50, max: 50 } ); + public readonly model = derivedDisposable(this, reader => { + if (this._editorObs.isReadonly.read(reader)) { return undefined; } + const textModel = this._editorObs.model.read(reader); + if (!textModel) { return undefined; } + + const model: InlineCompletionsModel = this._instantiationService.createInstance( + InlineCompletionsModel, + textModel, + this._suggestWidgetSelectedItem, + this._editorObs.versionId, + this._positions, + this._debounceValue, + observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.suggest).preview), + observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.suggest).previewMode), + observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).mode), + this._enabled, + ); + return model; + }).recomputeInitiallyAndOnChange(this._store); + + private readonly _ghostTexts = derived(this, (reader) => { + const model = this.model.read(reader); + return model?.ghostTexts.read(reader) ?? []; + }); + private readonly _stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); + + private readonly _ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => + store.add(this._instantiationService.createInstance(GhostTextWidget, this.editor, { + ghostText: ghostText, + minReservedLineCount: constObservable(0), + targetTextModel: this.model.map(v => v?.textModel), + })) + ).recomputeInitiallyAndOnChange(this._store); + private readonly _playAccessibilitySignal = observableSignal(this); - private readonly _isReadonly = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly)); - private readonly _textModel = observableFromEvent(this.editor.onDidChangeModel, () => this.editor.getModel()); - private readonly _textModelIfWritable = derived(reader => this._isReadonly.read(reader) ? undefined : this._textModel.read(reader)); + private readonly _fontFamily = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).fontFamily); constructor( public readonly editor: ICodeEditor, @@ -109,69 +132,11 @@ export class InlineCompletionsController extends Disposable { this._register(new InlineCompletionContextKeys(this._contextKeyService, this.model)); - this._register(autorun(reader => { - /** @description InlineCompletionsController.update model */ - const textModel = this._textModelIfWritable.read(reader); - transaction(tx => { - /** @description InlineCompletionsController.onDidChangeModel/readonly */ - this.model.set(undefined, tx); - this.updateObservables(tx, VersionIdChangeReason.Other); - - if (textModel) { - const model = _instantiationService.createInstance( - InlineCompletionsModel, - textModel, - this._suggestWidgetAdaptor.selectedItem, - this._textModelVersionId, - this._positions, - this._debounceValue, - observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.suggest).preview), - observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.suggest).previewMode), - observableFromEvent(editor.onDidChangeConfiguration, () => editor.getOption(EditorOption.inlineSuggest).mode), - this._enabled, - ); - this.model.set(model, tx); - } - }); - })); - - const styleElement = this._register(createStyleSheet2()); - this._register(autorun(reader => { - const fontFamily = this._fontFamily.read(reader); - styleElement.setStyle(fontFamily === '' || fontFamily === 'default' ? `` : ` -.monaco-editor .ghost-text-decoration, -.monaco-editor .ghost-text-decoration-preview, -.monaco-editor .ghost-text { - font-family: ${fontFamily}; -}`); - })); - - const getReason = (e: IModelContentChangedEvent): VersionIdChangeReason => { - if (e.isUndoing) { return VersionIdChangeReason.Undo; } - if (e.isRedoing) { return VersionIdChangeReason.Redo; } - if (this.model.get()?.isAcceptingPartially) { return VersionIdChangeReason.AcceptWord; } - return VersionIdChangeReason.Other; - }; - this._register(editor.onDidChangeModelContent((e) => transaction(tx => - /** @description InlineCompletionsController.onDidChangeModelContent */ - this.updateObservables(tx, getReason(e)) - ))); - - this._register(editor.onDidChangeCursorPosition(e => transaction(tx => { - /** @description InlineCompletionsController.onDidChangeCursorPosition */ - this.updateObservables(tx, VersionIdChangeReason.Other); - if (e.reason === CursorChangeReason.Explicit || e.source === 'api') { - this.model.get()?.stop(tx); - } - }))); - - this._register(editor.onDidType(() => transaction(tx => { - /** @description InlineCompletionsController.onDidType */ - this.updateObservables(tx, VersionIdChangeReason.Other); + this._register(reactToChange(this._editorObs.onDidType, (_value, _changes) => { if (this._enabled.get()) { - this.model.get()?.trigger(tx); + this.model.get()?.trigger(); } - }))); + })); this._register(this._commandService.onDidExecuteCommand((e) => { // These commands don't trigger onDidType. @@ -183,22 +148,28 @@ export class InlineCompletionsController extends Disposable { 'acceptSelectedSuggestion', ]); if (commands.has(e.commandId) && editor.hasTextFocus() && this._enabled.get()) { - transaction(tx => { + this._editorObs.forceUpdate(tx => { /** @description onDidExecuteCommand */ this.model.get()?.trigger(tx); }); } })); + this._register(reactToChange(this._editorObs.selections, (_value, changes) => { + if (changes.some(e => e.reason === CursorChangeReason.Explicit || e.source === 'api')) { + this.model.get()?.stop(); + } + })); + this._register(this.editor.onDidBlurEditorWidget(() => { // This is a hidden setting very useful for debugging - if (this._contextKeyService.getContextKeyValue('accessibleViewIsShown') || this._configurationService.getValue('editor.inlineSuggest.keepOnBlur') || - editor.getOption(EditorOption.inlineSuggest).keepOnBlur) { - return; - } - if (InlineSuggestionHintsContentWidget.dropDownVisible) { + if (this._contextKeyService.getContextKeyValue('accessibleViewIsShown') + || this._configurationService.getValue('editor.inlineSuggest.keepOnBlur') + || editor.getOption(EditorOption.inlineSuggest).keepOnBlur + || InlineSuggestionHintsContentWidget.dropDownVisible) { return; } + transaction(tx => { /** @description InlineCompletionsController.onDidBlurEditorWidget */ this.model.get()?.stop(tx); @@ -220,43 +191,48 @@ export class InlineCompletionsController extends Disposable { this._suggestWidgetAdaptor.stopForceRenderingAbove(); })); - const cancellationStore = this._register(new DisposableStore()); - let lastInlineCompletionId: string | undefined = undefined; - this._register(autorunHandleChanges({ - handleChange: (context, changeSummary) => { - if (context.didChange(this._playAccessibilitySignal)) { - lastInlineCompletionId = undefined; - } - return true; - }, - }, async (reader, _) => { - /** @description InlineCompletionsController.playAccessibilitySignalAndReadSuggestion */ - this._playAccessibilitySignal.read(reader); - + const currentInlineCompletionBySemanticId = derivedObservableWithCache(this, (reader, last) => { const model = this.model.read(reader); const state = model?.state.read(reader); - if (!model || !state || !state.inlineCompletion) { - lastInlineCompletionId = undefined; - return; + if (this._suggestWidgetSelectedItem.get()) { + return last; } + return state?.inlineCompletion?.semanticId; + }); + this._register(reactToChangeWithStore(derived(reader => { + this._playAccessibilitySignal.read(reader); + currentInlineCompletionBySemanticId.read(reader); + return {}; + }), async (_value, _deltas, store) => { + /** @description InlineCompletionsController.playAccessibilitySignalAndReadSuggestion */ + const model = this.model.get(); + const state = model?.state.get(); + if (!state || !model) { return; } + const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); - if (state.inlineCompletion.semanticId !== lastInlineCompletionId) { - cancellationStore.clear(); - lastInlineCompletionId = state.inlineCompletion.semanticId; - const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); + await timeout(50, cancelOnDispose(store)); + await waitForState(this._suggestWidgetSelectedItem, isUndefined, () => false, cancelOnDispose(store)); - await timeout(50, cancelOnDispose(cancellationStore)); - await waitForState(this._suggestWidgetAdaptor.selectedItem, isUndefined, () => false, cancelOnDispose(cancellationStore)); - - await this._accessibilitySignalService.playSignal(AccessibilitySignal.inlineSuggestion); - - if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { - this.provideScreenReaderUpdate(state.primaryGhostText.renderForScreenReader(lineText)); - } + await this._accessibilitySignalService.playSignal(AccessibilitySignal.inlineSuggestion); + if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { + this._provideScreenReaderUpdate(state.primaryGhostText.renderForScreenReader(lineText)); } })); this._register(new InlineCompletionsHintsWidget(this.editor, this.model, this._instantiationService)); + + this._register(createStyleSheetFromObservable(derived(reader => { + const fontFamily = this._fontFamily.read(reader); + if (fontFamily === '' || fontFamily === 'default') { return ''; } + return ` +.monaco-editor .ghost-text-decoration, +.monaco-editor .ghost-text-decoration-preview, +.monaco-editor .ghost-text { + font-family: ${fontFamily}; +}`; + }))); + + // TODO@hediet this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('accessibility.verbosity.inlineCompletions')) { this.editor.updateOptions({ inlineCompletionsAccessibilityVerbose: this._configurationService.getValue('accessibility.verbosity.inlineCompletions') }); @@ -269,25 +245,14 @@ export class InlineCompletionsController extends Disposable { this._playAccessibilitySignal.trigger(tx); } - private provideScreenReaderUpdate(content: string): void { + private _provideScreenReaderUpdate(content: string): void { const accessibleViewShowing = this._contextKeyService.getContextKeyValue('accessibleViewIsShown'); const accessibleViewKeybinding = this._keybindingService.lookupKeybinding('editor.action.accessibleView'); let hint: string | undefined; if (!accessibleViewShowing && accessibleViewKeybinding && this.editor.getOption(EditorOption.inlineCompletionsAccessibilityVerbose)) { hint = localize('showAccessibleViewHint', "Inspect this in the accessible view ({0})", accessibleViewKeybinding.getAriaLabel()); } - hint ? alert(content + ', ' + hint) : alert(content); - } - - /** - * Copies over the relevant state from the text model to observables. - * This solves all kind of eventing issues, as we make sure we always operate on the latest state, - * regardless of who calls into us. - */ - private updateObservables(tx: ITransaction, changeReason: VersionIdChangeReason): void { - const newModel = this.editor.getModel(); - this._textModelVersionId.set(newModel?.getVersionId() ?? -1, tx, changeReason); - this._positions.set(this.editor.getSelections()?.map(selection => selection.getPosition()) ?? [new Position(1, 1)], tx); + alert(hint ? content + ', ' + hint : content); } public shouldShowHoverAt(range: Range) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts index 9f7d3d17ba8..2226a1ea265 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts @@ -36,7 +36,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; export class InlineCompletionsHintsWidget extends Disposable { - private readonly alwaysShowToolbar = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always'); + private readonly alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always'); private sessionPosition: Position | undefined = undefined; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 7cb849da9db..92013fd5982 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Permutation } from 'vs/base/common/arrays'; +import { compareBy, Permutation } from 'vs/base/common/arrays'; import { mapFindFirst } from 'vs/base/common/arraysFind'; import { itemsEquals } from 'vs/base/common/equals'; import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; @@ -18,10 +18,12 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; import { TextLength } from 'vs/editor/common/core/textLength'; +import { ScrollType } from 'vs/editor/common/editorCommon'; import { Command, InlineCompletionContext, InlineCompletionTriggerKind, PartialAcceptTriggerKind } from 'vs/editor/common/languages'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource'; import { computeGhostText, singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; @@ -31,17 +33,10 @@ import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetCon import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -export enum VersionIdChangeReason { - Undo, - Redo, - AcceptWord, - Other, -} - export class InlineCompletionsModel extends Disposable { - private readonly _source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this.textModelVersionId, this._debounceValue)); - private readonly _isActive = observableValue(this, false); - readonly _forceUpdateExplicitlySignal = observableSignal(this); + private readonly _source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue)); + private readonly _isActive = observableValue(this, false); + private readonly _forceUpdateExplicitlySignal = observableSignal(this); // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. private readonly _selectedInlineCompletionId = observableValue(this, undefined); @@ -53,7 +48,7 @@ export class InlineCompletionsModel extends Disposable { constructor( public readonly textModel: ITextModel, public readonly selectedSuggestItem: IObservable, - public readonly textModelVersionId: IObservable, + public readonly _textModelVersionId: IObservable, private readonly _positions: IObservable, private readonly _debounceValue: IFeatureDebounceInformation, private readonly _suggestPreviewEnabled: IObservable, @@ -90,6 +85,13 @@ export class InlineCompletionsModel extends Disposable { VersionIdChangeReason.AcceptWord, ]); + private _getReason(e: IModelContentChangedEvent | undefined): VersionIdChangeReason { + if (e?.isUndoing) { return VersionIdChangeReason.Undo; } + if (e?.isRedoing) { return VersionIdChangeReason.Redo; } + if (this.isAcceptingPartially) { return VersionIdChangeReason.AcceptWord; } + return VersionIdChangeReason.Other; + } + private readonly _fetchInlineCompletionsPromise = derivedHandleChanges({ owner: this, createEmptyChangeSummary: () => ({ @@ -98,7 +100,7 @@ export class InlineCompletionsModel extends Disposable { }), handleChange: (ctx, changeSummary) => { /** @description fetch inline completions */ - if (ctx.didChange(this.textModelVersionId) && this._preserveCurrentCompletionReasons.has(ctx.change)) { + if (ctx.didChange(this._textModelVersionId) && this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) { changeSummary.preserveCurrentCompletion = true; } else if (ctx.didChange(this._forceUpdateExplicitlySignal)) { changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; @@ -113,7 +115,7 @@ export class InlineCompletionsModel extends Disposable { return undefined; } - this.textModelVersionId.read(reader); // Refetch on text change + this._textModelVersionId.read(reader); // Refetch on text change const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.get(); const suggestItem = this.selectedSuggestItem.read(reader); @@ -263,26 +265,24 @@ export class InlineCompletionsModel extends Disposable { const augmentedCompletion = mapFindFirst(candidateInlineCompletions, completion => { let r = completion.toSingleTextEdit(reader); - r = singleTextRemoveCommonPrefix(r, model, Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition())); + r = singleTextRemoveCommonPrefix( + r, + model, + Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition()) + ); return singleTextEditAugments(r, suggestCompletion) ? { completion, edit: r } : undefined; }); return augmentedCompletion; } - public readonly ghostTexts = derivedOpts({ - owner: this, - equalsFn: ghostTextsOrReplacementsEqual - }, reader => { + public readonly ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => { const v = this.state.read(reader); if (!v) { return undefined; } return v.ghostTexts; }); - public readonly primaryGhostText = derivedOpts({ - owner: this, - equalsFn: ghostTextOrReplacementEquals - }, reader => { + public readonly primaryGhostText = derivedOpts({ owner: this, equalsFn: ghostTextOrReplacementEquals }, reader => { const v = this.state.read(reader); if (!v) { return undefined; } return v?.primaryGhostText; @@ -319,6 +319,11 @@ export class InlineCompletionsModel extends Disposable { } const completion = state.inlineCompletion.toInlineCompletion(undefined); + if (completion.command) { + // Make sure the completion list will not be disposed. + completion.source.addRef(); + } + editor.pushUndoStop(); if (completion.snippetInfo) { editor.executeEdits( @@ -340,18 +345,8 @@ export class InlineCompletionsModel extends Disposable { editor.setSelections(selections, 'inlineCompletionAccept'); } - if (completion.command) { - // Make sure the completion list will not be disposed. - completion.source.addRef(); - } - - // Reset before invoking the command, since the command might cause a follow up trigger. - transaction(tx => { - this._source.clear(tx); - // Potentially, isActive will get set back to true by the typing or accept inline suggest event - // if automatic inline suggestions are enabled. - this._isActive.set(false, tx); - }); + // Reset before invoking the command, as the command might cause a follow up trigger (which we don't want to reset). + this.stop(); if (completion.command) { await this._commandService @@ -444,6 +439,7 @@ export class InlineCompletionsModel extends Disposable { const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p)); editor.executeEdits('inlineSuggestion.accept', edits.map(edit => EditOperation.replace(edit.range, edit.text))); editor.setSelections(selections, 'inlineCompletionPartialAccept'); + editor.revealPositionInCenterIfOutsideViewport(editor.getPosition()!, ScrollType.Immediate); } finally { this._isAcceptingPartially = false; } @@ -456,9 +452,7 @@ export class InlineCompletionsModel extends Disposable { completion.source.inlineCompletions, completion.sourceInlineCompletion, text.length, - { - kind, - } + { kind, } ); } } finally { @@ -483,6 +477,13 @@ export class InlineCompletionsModel extends Disposable { } } +export enum VersionIdChangeReason { + Undo, + Redo, + AcceptWord, + Other, +} + export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: SingleTextEdit): SingleTextEdit[] { if (positions.length === 1) { // No secondary cursor positions @@ -525,7 +526,7 @@ function substringPos(text: string, pos: Position): string { } function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { - const sortPerm = Permutation.createSortPermutation(edits, (edit1, edit2) => Range.compareRangesUsingStarts(edit1.range, edit2.range)); + const sortPerm = Permutation.createSortPermutation(edits, compareBy(e => e.range, Range.compareRangesUsingStarts)); const edit = new TextEdit(sortPerm.apply(edits)); const sortedNewRanges = edit.getNewRanges(); const newRanges = sortPerm.inverse().apply(sortedNewRanges); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts index 32b77dd23fe..1eb06aa11c1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource.ts @@ -27,7 +27,7 @@ export class InlineCompletionsSource extends Disposable { constructor( private readonly textModel: ITextModel, - private readonly versionId: IObservable, + private readonly versionId: IObservable, private readonly _debounceValue: IFeatureDebounceInformation, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService, @@ -62,7 +62,7 @@ export class InlineCompletionsSource extends Disposable { await wait(this._debounceValue.get(this.textModel), source.token); } - if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) { + if (source.token.isCancellationRequested || this._store.isDisposed || this.textModel.getVersionId() !== request.versionId) { return false; } @@ -76,7 +76,7 @@ export class InlineCompletionsSource extends Disposable { this.languageConfigurationService ); - if (source.token.isCancellationRequested || this.textModel.getVersionId() !== request.versionId) { + if (source.token.isCancellationRequested || this._store.isDisposed || this.textModel.getVersionId() !== request.versionId) { return false; } @@ -182,7 +182,7 @@ export class UpToDateInlineCompletions implements IDisposable { private readonly inlineCompletionProviderResult: InlineCompletionProviderResult, public readonly request: UpdateRequest, private readonly _textModel: ITextModel, - private readonly _versionId: IObservable, + private readonly _versionId: IObservable, ) { const ids = _textModel.deltaDecorations([], inlineCompletionProviderResult.completions.map(i => ({ range: i.range, @@ -254,7 +254,7 @@ export class InlineCompletionWithUpdatedRange { public readonly inlineCompletion: InlineCompletionItem, public readonly decorationId: string, private readonly _textModel: ITextModel, - private readonly _modelVersion: IObservable, + private readonly _modelVersion: IObservable, ) { } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts index 28052040c32..25ccf4cd125 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts @@ -18,19 +18,19 @@ import { ILanguageConfigurationService } from 'vs/editor/common/languages/langua import { ITextModel } from 'vs/editor/common/model'; import { fixBracketsInLine } from 'vs/editor/common/model/bracketPairsTextModelPart/fixBrackets'; import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; -import { getReadonlyEmptyArray } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +import { getReadonlyEmptyArray } from './utils'; import { SnippetParser, Text } from 'vs/editor/contrib/snippet/browser/snippetParser'; export async function provideInlineCompletions( registry: LanguageFeatureRegistry, - position: Position, + positionOrRange: Position | Range, model: ITextModel, context: InlineCompletionContext, token: CancellationToken = CancellationToken.None, languageConfigurationService?: ILanguageConfigurationService, ): Promise { // Important: Don't use position after the await calls, as the model could have been changed in the meantime! - const defaultReplaceRange = getDefaultRange(position, model); + const defaultReplaceRange = positionOrRange instanceof Position ? getDefaultRange(positionOrRange, model) : positionOrRange; const providers = registry.all(model); const multiMap = new SetMap>(); @@ -100,8 +100,13 @@ export async function provideInlineCompletions( } try { - const completions = await provider.provideInlineCompletions(model, position, context, token); - return completions; + if (positionOrRange instanceof Position) { + const completions = await provider.provideInlineCompletions(model, positionOrRange, context, token); + return completions; + } else { + const completions = await provider.provideInlineEdits?.(model, positionOrRange, context, token); + return completions; + } } catch (e) { onUnexpectedExternalError(e); return undefined; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts index 95d135ce324..daf73173ef4 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts @@ -3,39 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; +import { compareBy, numberComparator } from 'vs/base/common/arrays'; +import { findFirstMax } from 'vs/base/common/arraysFind'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; import { CompletionItemInsertTextRule, CompletionItemKind, SelectedSuggestionInfo } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { SnippetSession } from 'vs/editor/contrib/snippet/browser/snippetSession'; import { CompletionItem } from 'vs/editor/contrib/suggest/browser/suggest'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; -import { IObservable, ITransaction, observableValue, transaction } from 'vs/base/common/observable'; -import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; -import { ITextModel } from 'vs/editor/common/model'; -import { compareBy, numberComparator } from 'vs/base/common/arrays'; -import { findFirstMax } from 'vs/base/common/arraysFind'; -import { singleTextEditAugments, singleTextRemoveCommonPrefix } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; export class SuggestWidgetAdaptor extends Disposable { private isSuggestWidgetVisible: boolean = false; private isShiftKeyPressed = false; private _isActive = false; private _currentSuggestItemInfo: SuggestItemInfo | undefined = undefined; - - private readonly _selectedItem = observableValue(this, undefined as SuggestItemInfo | undefined); - - public get selectedItem(): IObservable { - return this._selectedItem; + public get selectedItem(): SuggestItemInfo | undefined { + return this._currentSuggestItemInfo; } + private _onDidSelectedItemChange = this._register(new Emitter()); + public readonly onDidSelectedItemChange: Event = this._onDidSelectedItemChange.event; constructor( private readonly editor: ICodeEditor, private readonly suggestControllerPreselector: () => SingleTextEdit | undefined, - private readonly checkModelVersion: (tx: ITransaction) => void, private readonly onWillAccept: (item: SuggestItemInfo) => void, ) { super(); @@ -59,8 +56,6 @@ export class SuggestWidgetAdaptor extends Disposable { this._register(suggestController.registerSelector({ priority: 100, select: (model, pos, suggestItems) => { - transaction(tx => this.checkModelVersion(tx)); - const textModel = this.editor.getModel(); if (!textModel) { // Should not happen @@ -142,11 +137,7 @@ export class SuggestWidgetAdaptor extends Disposable { this._isActive = newActive; this._currentSuggestItemInfo = newInlineCompletion; - transaction(tx => { - /** @description Update state from suggest widget */ - this.checkModelVersion(tx); - this._selectedItem.set(this._isActive ? this._currentSuggestItemInfo : undefined, tx); - }); + this._onDidSelectedItemChange.fire(); } } diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts index 648f5940596..63ab19affbe 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { getSecondaryEdits } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts index e160c3daa01..e318702da15 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts index 354fa36b5af..a860898c64e 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts @@ -25,7 +25,7 @@ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import * as assert from 'assert'; +import assert from 'assert'; import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; diff --git a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts index 298fcd4452e..9a4c3a927b4 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts @@ -16,6 +16,7 @@ import { InlineDecorationType } from 'vs/editor/common/viewModel'; import { AdditionalLinesWidget, LineData } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextWidget'; import { GhostText } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { ColumnRange, applyObservableDecorations } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +import { diffDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; export const INLINE_EDIT_DESCRIPTION = 'inline-edit'; export interface IGhostTextWidgetModel { @@ -23,12 +24,11 @@ export interface IGhostTextWidgetModel { readonly ghostText: IObservable; readonly minReservedLineCount: IObservable; readonly range: IObservable; - readonly backgroundColoring: IObservable; } export class GhostTextWidget extends Disposable { private readonly isDisposed = observableValue(this, false); - private readonly currentTextModel = observableFromEvent(this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); + private readonly currentTextModel = observableFromEvent(this, this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); constructor( private readonly editor: ICodeEditor, @@ -92,7 +92,7 @@ export class GhostTextWidget extends Disposable { let hiddenTextStartColumn: number | undefined = undefined; let lastIdx = 0; - if (!isPureRemove) { + if (!isPureRemove && (isSingleLine || !range)) { for (const part of ghostText.parts) { let lines = part.lines; //If remove range is set, we want to push all new liens to virtual area @@ -140,7 +140,6 @@ export class GhostTextWidget extends Disposable { range, isSingleLine, isPureRemove, - backgroundColoring: this.model.backgroundColoring.read(reader) }; }); @@ -184,11 +183,10 @@ export class GhostTextWidget extends Disposable { ranges.push(range); } } - const className = uiState.backgroundColoring ? 'inline-edit-remove backgroundColoring' : 'inline-edit-remove'; for (const range of ranges) { decorations.push({ range, - options: { inlineClassName: className, description: 'inline-edit-remove', } + options: diffDeleteDecoration }); } } @@ -215,7 +213,7 @@ export class GhostTextWidget extends Disposable { derived(reader => { /** @description lines */ const uiState = this.uiState.read(reader); - return uiState && !uiState.isPureRemove ? { + return uiState && !uiState.isPureRemove && (uiState.isSingleLine || !uiState.range) ? { lineNumber: uiState.lineNumber, additionalLines: uiState.additionalLines, minReservedLineCount: uiState.additionalReservedLineCount, diff --git a/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts b/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts index 6c1c7337f7f..cfacc77329a 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/hoverParticipant.ts @@ -3,17 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { constObservable } from 'vs/base/common/observable'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { IModelDecoration } from 'vs/editor/common/model'; -import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { InlineEditController } from 'vs/editor/contrib/inlineEdit/browser/inlineEditController'; import { InlineEditHintsContentWidget } from 'vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget'; +import * as nls from 'vs/nls'; export class InlineEditHover implements IHoverPart { constructor( @@ -86,8 +87,8 @@ export class InlineEditHoverParticipant implements IEditorHoverParticipant { + const disposables = new DisposableStore(); this._telemetryService.publicLog2<{}, { owner: 'hediet'; @@ -97,9 +98,17 @@ export class InlineEditHoverParticipant implements IEditorHoverParticipant = { + hoverPart: hoverParts[0], + hoverElement: widgetNode, + dispose: () => disposables.dispose() + }; + return new RenderedHoverParts([renderedHoverPart]); + } - return disposableStore; + getAccessibleContent(hoverPart: InlineEditHover): string { + return nls.localize('hoverAccessibilityInlineEdits', 'There are inline edits here.'); } } diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution.ts index 7196773a7cf..0be0d1254c0 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; +// import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { AcceptInlineEdit, JumpBackInlineEdit, JumpToInlineEdit, RejectInlineEdit, TriggerInlineEdit } from 'vs/editor/contrib/inlineEdit/browser/commands'; -import { InlineEditHoverParticipant } from 'vs/editor/contrib/inlineEdit/browser/hoverParticipant'; +// import { InlineEditHoverParticipant } from 'vs/editor/contrib/inlineEdit/browser/hoverParticipant'; import { InlineEditController } from 'vs/editor/contrib/inlineEdit/browser/inlineEditController'; registerEditorAction(AcceptInlineEdit); @@ -17,4 +17,4 @@ registerEditorAction(TriggerInlineEdit); registerEditorContribution(InlineEditController.ID, InlineEditController, EditorContributionInstantiation.Eventually); -HoverParticipantRegistry.register(InlineEditHoverParticipant); +// HoverParticipantRegistry.register(InlineEditHoverParticipant); diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.css b/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.css index d6d156544e0..aced5d6271e 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.css +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEdit.css @@ -6,11 +6,6 @@ .monaco-editor .inline-edit-remove { background-color: var(--vscode-editorGhostText-background); font-style: italic; - text-decoration: line-through; -} - -.monaco-editor .inline-edit-remove.backgroundColoring { - background-color: var(--vscode-diffEditor-removedLineBackground); } .monaco-editor .inline-edit-hidden { diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts index 283e72be068..4e231fd5162 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { ISettableObservable, autorun, constObservable, disposableObservableValue, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { ISettableObservable, autorun, constObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; @@ -22,39 +22,57 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { createStyleSheet2 } from 'vs/base/browser/dom'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; - -export class InlineEditWidget implements IDisposable { - constructor(public readonly widget: GhostTextWidget, public readonly edit: IInlineEdit) { } - - dispose(): void { - this.widget.dispose(); - } -} +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; +import { InlineEditSideBySideWidget } from 'vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget'; +import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; +import { IModelService } from 'vs/editor/common/services/model'; export class InlineEditController extends Disposable { static ID = 'editor.contrib.inlineEditController'; public static readonly inlineEditVisibleKey = 'inlineEditVisible'; - public static readonly inlineEditVisibleContext = new RawContextKey(InlineEditController.inlineEditVisibleKey, false); + public static readonly inlineEditVisibleContext = new RawContextKey(this.inlineEditVisibleKey, false); private _isVisibleContext = InlineEditController.inlineEditVisibleContext.bindTo(this.contextKeyService); public static readonly cursorAtInlineEditKey = 'cursorAtInlineEdit'; - public static readonly cursorAtInlineEditContext = new RawContextKey(InlineEditController.cursorAtInlineEditKey, false); + public static readonly cursorAtInlineEditContext = new RawContextKey(this.cursorAtInlineEditKey, false); private _isCursorAtInlineEditContext = InlineEditController.cursorAtInlineEditContext.bindTo(this.contextKeyService); public static get(editor: ICodeEditor): InlineEditController | null { return editor.getContribution(InlineEditController.ID); } - private _currentEdit: ISettableObservable = this._register(disposableObservableValue(this, undefined)); + private _currentEdit: ISettableObservable = observableValue(this, undefined); + private _currentWidget = derivedDisposable(this._currentEdit, (reader) => { + const edit = this._currentEdit.read(reader); + if (!edit) { + return undefined; + } + const line = edit.range.endLineNumber; + const column = edit.range.endColumn; + const textToDisplay = edit.text.endsWith('\n') && !(edit.range.startLineNumber === edit.range.endLineNumber && edit.range.startColumn === edit.range.endColumn) ? edit.text.slice(0, -1) : edit.text; + const ghostText = new GhostText(line, [new GhostTextPart(column, textToDisplay, false)]); + //only show ghost text for single line edits + //multi line edits are shown in the side by side widget + const isSingleLine = edit.range.startLineNumber === edit.range.endLineNumber && ghostText.parts.length === 1 && ghostText.parts[0].lines.length === 1; + if (!isSingleLine) { + return undefined; + } + const instance = this.instantiationService.createInstance(GhostTextWidget, this.editor, { + ghostText: constObservable(ghostText), + minReservedLineCount: constObservable(0), + targetTextModel: constObservable(this.editor.getModel() ?? undefined), + range: constObservable(edit.range) + }); + return instance; + }); private _currentRequestCts: CancellationTokenSource | undefined; private _jumpBackPosition: Position | undefined; private _isAccepting: ISettableObservable = observableValue(this, false); - private readonly _enabled = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled); - private readonly _fontFamily = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).fontFamily); - private readonly _backgroundColoring = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).backgroundColoring); + private readonly _enabled = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled); + private readonly _fontFamily = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).fontFamily); constructor( @@ -64,6 +82,8 @@ export class InlineEditController extends Disposable { @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService, + @IModelService private readonly _modelService: IModelService, ) { super(); @@ -84,7 +104,7 @@ export class InlineEditController extends Disposable { })); //Check if the cursor is at the ghost text - const cursorPosition = observableFromEvent(editor.onDidChangeCursorPosition, () => editor.getPosition()); + const cursorPosition = observableFromEvent(this, editor.onDidChangeCursorPosition, () => editor.getPosition()); this._register(autorun(reader => { /** @description InlineEditController.cursorPositionChanged model */ if (!this._enabled.read(reader)) { @@ -154,7 +174,8 @@ export class InlineEditController extends Disposable { }`); })); - this._register(new InlineEditHintsWidget(this.editor, this._currentEdit, this.instantiationService)); + this._register(new InlineEditHintsWidget(this.editor, this._currentWidget, this.instantiationService)); + this._register(new InlineEditSideBySideWidget(this.editor, this._currentEdit, this.instantiationService, this._diffProviderFactoryService, this._modelService)); } private checkCursorPosition(position: Position) { @@ -162,7 +183,7 @@ export class InlineEditController extends Disposable { this._isCursorAtInlineEditContext.set(false); return; } - const gt = this._currentEdit.get()?.edit; + const gt = this._currentEdit.get(); if (!gt) { this._isCursorAtInlineEditContext.set(false); return; @@ -231,18 +252,7 @@ export class InlineEditController extends Disposable { if (!edit) { return; } - const line = edit.range.endLineNumber; - const column = edit.range.endColumn; - const textToDisplay = edit.text.endsWith('\n') && !(edit.range.startLineNumber === edit.range.endLineNumber && edit.range.startColumn === edit.range.endColumn) ? edit.text.slice(0, -1) : edit.text; - const ghostText = new GhostText(line, [new GhostTextPart(column, textToDisplay, false)]); - const instance = this.instantiationService.createInstance(GhostTextWidget, this.editor, { - ghostText: constObservable(ghostText), - minReservedLineCount: constObservable(0), - targetTextModel: constObservable(this.editor.getModel() ?? undefined), - range: constObservable(edit.range), - backgroundColoring: this._backgroundColoring - }); - this._currentEdit.set(new InlineEditWidget(instance, edit), undefined); + this._currentEdit.set(edit, undefined); } public async trigger() { @@ -260,7 +270,7 @@ export class InlineEditController extends Disposable { public async accept() { this._isAccepting.set(true, undefined); - const data = this._currentEdit.get()?.edit; + const data = this._currentEdit.get(); if (!data) { return; } @@ -287,7 +297,7 @@ export class InlineEditController extends Disposable { public jumpToCurrent(): void { this._jumpBackPosition = this.editor.getSelection()?.getStartPosition(); - const data = this._currentEdit.get()?.edit; + const data = this._currentEdit.get(); if (!data) { return; } @@ -298,7 +308,7 @@ export class InlineEditController extends Disposable { } public async clear(sendRejection: boolean = true) { - const edit = this._currentEdit.get()?.edit; + const edit = this._currentEdit.get(); if (edit && edit?.rejected && sendRejection) { await this._commandService .executeCommand(edit.rejected.id, ...(edit.rejected.arguments || [])) @@ -324,11 +334,15 @@ export class InlineEditController extends Disposable { public shouldShowHoverAt(range: Range) { const currentEdit = this._currentEdit.get(); + const currentWidget = this._currentWidget.get(); if (!currentEdit) { return false; } - const edit = currentEdit.edit; - const model = currentEdit.widget.model; + if (!currentWidget) { + return false; + } + const edit = currentEdit; + const model = currentWidget.model; const overReplaceRange = Range.containsPosition(edit.range, range.getStartPosition()) || Range.containsPosition(edit.range, range.getEndPosition()); if (overReplaceRange) { return true; @@ -341,7 +355,7 @@ export class InlineEditController extends Disposable { } public shouldShowHoverAtViewZone(viewZoneId: string): boolean { - return this._currentEdit.get()?.widget.ownsViewZone(viewZoneId) ?? false; + return this._currentWidget.get()?.ownsViewZone(viewZoneId) ?? false; } } diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts index ee0b537a88c..cb2453ab6c6 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditHintsWidget.ts @@ -15,7 +15,7 @@ import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentW import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { PositionAffinity } from 'vs/editor/common/model'; -import { InlineEditWidget } from 'vs/editor/contrib/inlineEdit/browser/inlineEditController'; +import { GhostTextWidget } from 'vs/editor/contrib/inlineEdit/browser/ghostTextWidget'; import { MenuEntryActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; @@ -27,12 +27,12 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class InlineEditHintsWidget extends Disposable { - private readonly alwaysShowToolbar = observableFromEvent(this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).showToolbar === 'always'); + private readonly alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).showToolbar === 'always'); private sessionPosition: Position | undefined = undefined; private readonly position = derived(this, reader => { - const ghostText = this.model.read(reader)?.widget.model.ghostText.read(reader); + const ghostText = this.model.read(reader)?.model.ghostText.read(reader); if (!this.alwaysShowToolbar.read(reader) || !ghostText || ghostText.parts.length === 0) { this.sessionPosition = undefined; @@ -51,7 +51,7 @@ export class InlineEditHintsWidget extends Disposable { constructor( private readonly editor: ICodeEditor, - private readonly model: IObservable, + private readonly model: IObservable, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); diff --git a/build/azure-pipelines/common/installPlaywright.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.css similarity index 55% rename from build/azure-pipelines/common/installPlaywright.ts rename to src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.css index 742b6e0e399..bc7e553e4d8 100644 --- a/build/azure-pipelines/common/installPlaywright.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.css @@ -3,12 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -process.env.DEBUG='pw:install'; // enable logging for this (https://github.com/microsoft/playwright/issues/17394) - -const { installDefaultBrowsersForNpmInstall } = require('playwright-core/lib/server'); - -async function install() { - await installDefaultBrowsersForNpmInstall(); +.monaco-editor .inlineEditSideBySide { + z-index: 39; + color: var(--vscode-editorHoverWidget-foreground); + background-color: var(--vscode-editorHoverWidget-background); + border: 1px solid var(--vscode-editorHoverWidget-border); + white-space: pre; } - -install(); diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts new file mode 100644 index 00000000000..bd10e0669b4 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts @@ -0,0 +1,346 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ObservablePromise, autorun, autorunWithStore, derived, observableSignalFromEvent } from 'vs/base/common/observable'; +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./inlineEditSideBySideWidget'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; +import { diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecoration, diffDeleteDecorationEmpty, diffLineDeleteDecorationBackgroundWithIndicator, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { IInlineEdit } from 'vs/editor/common/languages'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { IModelService } from 'vs/editor/common/services/model'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +function* range(start: number, end: number, step = 1) { + if (end === undefined) { [end, start] = [start, 0]; } + for (let n = start; n < end; n += step) { yield n; } +} + +function removeIndentation(lines: string[]): string[] { + const indentation = lines[0].match(/^\s*/)?.[0] ?? ''; + return lines.map(l => l.replace(new RegExp('^' + indentation), '')); +} + +type Pos = { + top: number; + left: Position; +}; + +export class InlineEditSideBySideWidget extends Disposable { + private static _modelId = 0; + private static _createUniqueUri(): URI { + return URI.from({ scheme: 'inline-edit-widget', path: new Date().toString() + String(InlineEditSideBySideWidget._modelId++) }); + } + + private readonly _position = derived(this, reader => { + const ghostText = this._model.read(reader); + + if (!ghostText || ghostText.text.length === 0) { + return null; + } + if (ghostText.range.startLineNumber === ghostText.range.endLineNumber) { + //for inner-line suggestions we still want to use minimal ghost text + return null; + } + const editorModel = this._editor.getModel(); + if (!editorModel) { + return null; + } + const lines = Array.from(range(ghostText.range.startLineNumber, ghostText.range.endLineNumber + 1)); + const lengths = lines.map(lineNumber => editorModel.getLineLastNonWhitespaceColumn(lineNumber)); + const maxColumn = Math.max(...lengths); + const lineOfMaxColumn = lines[lengths.indexOf(maxColumn)]; + + const position = new Position(lineOfMaxColumn, maxColumn); + const pos = { + top: ghostText.range.startLineNumber, + left: position + }; + + return pos; + }); + + private readonly _text = derived(this, reader => { + const ghostText = this._model.read(reader); + if (!ghostText) { + return ''; + } + return removeIndentation(ghostText.text.split('\n')).join('\n'); + }); + + + private readonly _originalModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditSideBySideWidget._createUniqueUri())).keepObserved(this._store); + private readonly _modifiedModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditSideBySideWidget._createUniqueUri())).keepObserved(this._store); + + private readonly _diff = derived(this, reader => { + return this._diffPromise.read(reader)?.promiseResult.read(reader)?.data; + }); + + private readonly _diffPromise = derived(this, reader => { + const ghostText = this._model.read(reader); + if (!ghostText) { + return; + } + const editorModel = this._editor.getModel(); + if (!editorModel) { + return; + } + const originalText = removeIndentation(editorModel.getValueInRange(ghostText.range).split('\n')).join('\n'); + const modifiedText = removeIndentation(ghostText.text.split('\n')).join('\n'); + this._originalModel.get().setValue(originalText); + this._modifiedModel.get().setValue(modifiedText); + const d = this._diffProviderFactoryService.createDiffProvider({ diffAlgorithm: 'advanced' }); + return ObservablePromise.fromFn(async () => { + const result = await d.computeDiff(this._originalModel.get(), this._modifiedModel.get(), { + computeMoves: false, + ignoreTrimWhitespace: false, + maxComputationTimeMs: 1000, + }, CancellationToken.None); + + if (result.identical) { + return undefined; + } + + return result.changes; + }); + }); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _model: IObservable, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService, + @IModelService private readonly _modelService: IModelService, + ) { + super(); + + this._register(autorunWithStore((reader, store) => { + /** @description setup content widget */ + const model = this._model.read(reader); + if (!model) { + return; + } + + const contentWidget = store.add(this._instantiationService.createInstance( + InlineEditSideBySideContentWidget, + this._editor, + this._position, + this._text, + this._diff + )); + _editor.addOverlayWidget(contentWidget); + store.add(toDisposable(() => _editor.removeOverlayWidget(contentWidget))); + })); + } +} + +class InlineEditSideBySideContentWidget extends Disposable implements IOverlayWidget { + private static _dropDownVisible = false; + public static get dropDownVisible() { return this._dropDownVisible; } + + private static id = 0; + + private readonly id = `InlineEditSideBySideContentWidget${InlineEditSideBySideContentWidget.id++}`; + public readonly allowEditorOverflow = true; + public readonly suppressMouseDown = false; + + private readonly _nodes = $('div.inlineEditSideBySide', undefined,); + + private readonly _scrollChanged = observableSignalFromEvent('editor.onDidScrollChange', this._editor.onDidScrollChange); + + private readonly _previewEditor = this._register(this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this._nodes, + { + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + guides: { + indentation: false, + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveIndentation: false, + }, + folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + columnSelection: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + scrollbar: { vertical: 'hidden', horizontal: 'hidden' }, + }, + { contributions: [], }, + this._editor + )); + + private readonly _previewEditorObs = observableCodeEditor(this._previewEditor); + private readonly _editorObs = observableCodeEditor(this._editor); + + private readonly _previewTextModel = this._register(this._instantiationService.createInstance( + TextModel, + '', + this._editor.getModel()?.getLanguageId() ?? PLAINTEXT_LANGUAGE_ID, + TextModel.DEFAULT_CREATION_OPTIONS, + null + )); + + private readonly _setText = derived(reader => { + const edit = this._text.read(reader); + if (!edit) { return; } + this._previewTextModel.setValue(edit); + }).recomputeInitiallyAndOnChange(this._store); + + + private readonly _decorations = derived(this, (reader) => { + this._setText.read(reader); + const position = this._position.read(reader); + if (!position) { return { org: [], mod: [] }; } + const diff = this._diff.read(reader); + if (!diff) { return { org: [], mod: [] }; } + + const originalDecorations: IModelDeltaDecoration[] = []; + const modifiedDecorations: IModelDeltaDecoration[] = []; + + if (diff.length === 1 && diff[0].innerChanges![0].modifiedRange.equalsRange(this._previewTextModel.getFullModelRange())) { + return { org: [], mod: [] }; + } + + const moveRange = (range: IRange) => { + return new Range(range.startLineNumber + position.top - 1, range.startColumn, range.endLineNumber + position.top - 1, range.endColumn); + }; + + for (const m of diff) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: moveRange(m.original.toInclusiveRange()!), options: diffLineDeleteDecorationBackgroundWithIndicator }); + } + if (!m.modified.isEmpty) { + // modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffLineAddDecorationBackgroundWithIndicator }); + } + + if (m.modified.isEmpty || m.original.isEmpty) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: moveRange(m.original.toInclusiveRange()!), options: diffWholeLineDeleteDecoration }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration }); + } + } else { + for (const i of m.innerChanges || []) { + // Don't show empty markers outside the line range + if (m.original.contains(i.originalRange.startLineNumber)) { + originalDecorations.push({ range: moveRange(i.originalRange), options: i.originalRange.isEmpty() ? diffDeleteDecorationEmpty : diffDeleteDecoration }); + } + if (m.modified.contains(i.modifiedRange.startLineNumber)) { + modifiedDecorations.push({ range: i.modifiedRange, options: i.modifiedRange.isEmpty() ? diffAddDecorationEmpty : diffAddDecoration }); + } + } + } + } + + return { org: originalDecorations, mod: modifiedDecorations }; + }); + + private readonly _originalDecorations = derived(this, reader => { + return this._decorations.read(reader).org; + }); + + private readonly _modifiedDecorations = derived(this, reader => { + return this._decorations.read(reader).mod; + }); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _position: IObservable, + private readonly _text: IObservable, + private readonly _diff: IObservable, + + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + this._previewEditor.setModel(this._previewTextModel); + + this._register(this._editorObs.setDecorations(this._originalDecorations)); + this._register(this._previewEditorObs.setDecorations(this._modifiedDecorations)); + + this._register(autorun(reader => { + const width = this._previewEditorObs.contentWidth.read(reader); + const lines = this._text.get().split('\n').length - 1; + const height = this._editor.getOption(EditorOption.lineHeight) * lines; + if (width <= 0) { + return; + } + console.log('width', width); + this._previewEditor.layout({ height: height, width: width }); + })); + + this._register(autorun(reader => { + /** @description update position */ + this._position.read(reader); + this._editor.layoutOverlayWidget(this); + })); + + this._register(autorun(reader => { + /** @description scroll change */ + this._scrollChanged.read(reader); + const position = this._position.read(reader); + if (!position) { + return; + } + const visibleRanges = this._editor.getVisibleRanges(); + const isVisble = visibleRanges.some(range => { + return position.top >= range.startLineNumber && position.top <= range.endLineNumber; + }); + if (!isVisble) { + this._nodes.style.display = 'none'; + } + else { + this._nodes.style.display = 'block'; + } + this._editor.layoutOverlayWidget(this); + })); + } + + getId(): string { return this.id; } + + getDomNode(): HTMLElement { + return this._nodes; + } + + getPosition(): IOverlayWidgetPosition | null { + const position = this._position.get(); + if (!position) { + return null; + } + const layoutInfo = this._editor.getLayoutInfo(); + const visibPos = this._editor.getScrolledVisiblePosition(new Position(position.top, 1)); + if (!visibPos) { + return null; + } + const top = visibPos.top; + const left = layoutInfo.contentLeft + this._editor.getOffsetForColumn(position.left.lineNumber, position.left.column) + 10; + return { + preference: { + left, + top, + } + }; + } +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/commands.ts b/src/vs/editor/contrib/inlineEdits/browser/commands.ts new file mode 100644 index 00000000000..ec7255fd9f0 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/commands.ts @@ -0,0 +1,185 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { transaction } from 'vs/base/common/observable'; +import { asyncTransaction } from 'vs/base/common/observableInternal/base'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { inlineEditAcceptId, inlineEditVisible, showNextInlineEditActionId, showPreviousInlineEditActionId } from 'vs/editor/contrib/inlineEdits/browser/consts'; +import { InlineEditsController } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsController'; +import * as nls from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; + + +function labelAndAlias(str: nls.ILocalizedString): { label: string; alias: string } { + return { + label: str.value, + alias: str.original, + }; +} + +export class ShowNextInlineEditAction extends EditorAction { + public static ID = showNextInlineEditActionId; + constructor() { + super({ + id: ShowNextInlineEditAction.ID, + ...labelAndAlias(nls.localize2('action.inlineEdits.showNext', "Show Next Inline Edit")), + precondition: ContextKeyExpr.and(EditorContextKeys.writable, inlineEditVisible), + kbOpts: { + weight: 100, + primary: KeyMod.Alt | KeyCode.BracketRight, + }, + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + controller?.model.get()?.next(); + } +} + +export class ShowPreviousInlineEditAction extends EditorAction { + public static ID = showPreviousInlineEditActionId; + constructor() { + super({ + id: ShowPreviousInlineEditAction.ID, + ...labelAndAlias(nls.localize2('action.inlineEdits.showPrevious', "Show Previous Inline Edit")), + precondition: ContextKeyExpr.and(EditorContextKeys.writable, inlineEditVisible), + kbOpts: { + weight: 100, + primary: KeyMod.Alt | KeyCode.BracketLeft, + }, + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + controller?.model.get()?.previous(); + } +} + +export class TriggerInlineEditAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.inlineEdits.trigger', + ...labelAndAlias(nls.localize2('action.inlineEdits.trigger', "Trigger Inline Edit")), + precondition: EditorContextKeys.writable + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + await asyncTransaction(async tx => { + /** @description triggerExplicitly from command */ + await controller?.model.get()?.triggerExplicitly(tx); + }); + } +} + +export class AcceptInlineEdit extends EditorAction { + constructor() { + super({ + id: inlineEditAcceptId, + ...labelAndAlias(nls.localize2('action.inlineEdits.accept', "Accept Inline Edit")), + precondition: inlineEditVisible, + menuOpts: { + menuId: MenuId.InlineEditsActions, + title: nls.localize('inlineEditsActions', "Accept Inline Edit"), + group: 'primary', + order: 1, + icon: Codicon.check, + }, + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.Space, + weight: 20000, + kbExpr: inlineEditVisible, + } + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + if (editor instanceof EmbeddedCodeEditorWidget) { + editor = editor.getParentEditor(); + } + const controller = InlineEditsController.get(editor); + if (controller) { + controller.model.get()?.accept(controller.editor); + controller.editor.focus(); + } + } +} + +/* +TODO@hediet +export class PinInlineEdit extends EditorAction { + constructor() { + super({ + id: 'editor.action.inlineEdits.pin', + ...labelAndAlias(nls.localize2('action.inlineEdits.pin', "Pin Inline Edit")), + precondition: undefined, + kbOpts: { + primary: KeyMod.Shift | KeyCode.Space, + weight: 20000, + } + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + if (controller) { + controller.model.get()?.togglePin(); + } + } +} + +MenuRegistry.appendMenuItem(MenuId.InlineEditsActions, { + command: { + id: 'editor.action.inlineEdits.pin', + title: nls.localize('Pin', "Pin"), + icon: Codicon.pin, + }, + group: 'primary', + order: 1, + when: isPinnedContextKey.negate(), +}); + +MenuRegistry.appendMenuItem(MenuId.InlineEditsActions, { + command: { + id: 'editor.action.inlineEdits.unpin', + title: nls.localize('Unpin', "Unpin"), + icon: Codicon.pinned, + }, + group: 'primary', + order: 1, + when: isPinnedContextKey, +});*/ + +export class HideInlineEdit extends EditorAction { + public static ID = 'editor.action.inlineEdits.hide'; + + constructor() { + super({ + id: HideInlineEdit.ID, + ...labelAndAlias(nls.localize2('action.inlineEdits.hide', "Hide Inline Edit")), + precondition: inlineEditVisible, + kbOpts: { + weight: 100, + primary: KeyCode.Escape, + } + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineEditsController.get(editor); + transaction(tx => { + controller?.model.get()?.stop(tx); + }); + } +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/consts.ts b/src/vs/editor/contrib/inlineEdits/browser/consts.ts new file mode 100644 index 00000000000..9ad19e98a76 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/consts.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const inlineEditAcceptId = 'editor.action.inlineEdits.accept'; + +export const showPreviousInlineEditActionId = 'editor.action.inlineEdits.showPrevious'; + +export const showNextInlineEditActionId = 'editor.action.inlineEdits.showNext'; + +export const inlineEditVisible = new RawContextKey('inlineEditsVisible', false, localize('inlineEditsVisible', "Whether an inline edit is visible")); +export const isPinnedContextKey = new RawContextKey('inlineEditsIsPinned', false, localize('isPinned', "Whether an inline edit is visible")); diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution.ts new file mode 100644 index 00000000000..ae8b7182a89 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { + TriggerInlineEditAction, ShowNextInlineEditAction, ShowPreviousInlineEditAction, + AcceptInlineEdit, HideInlineEdit, +} from 'vs/editor/contrib/inlineEdits/browser/commands'; +import { InlineEditsController } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsController'; + +registerEditorContribution(InlineEditsController.ID, InlineEditsController, EditorContributionInstantiation.Eventually); + +registerEditorAction(TriggerInlineEditAction); +registerEditorAction(ShowNextInlineEditAction); +registerEditorAction(ShowPreviousInlineEditAction); +registerEditorAction(AcceptInlineEdit); +registerEditorAction(HideInlineEdit); diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts new file mode 100644 index 00000000000..9055ec56719 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { derived, derivedObservableWithCache, IReader, ISettableObservable, observableValue } from 'vs/base/common/observable'; +import { derivedDisposable, derivedWithSetter } from 'vs/base/common/observableInternal/derived'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; +import { readHotReloadableExport } from 'vs/base/common/hotReloadHelpers'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { inlineEditVisible, isPinnedContextKey } from 'vs/editor/contrib/inlineEdits/browser/consts'; +import { InlineEditsModel } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsModel'; +import { InlineEditsWidget } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsWidget'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { bindContextKey, observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; + +export class InlineEditsController extends Disposable { + static ID = 'editor.contrib.inlineEditsController'; + + public static get(editor: ICodeEditor): InlineEditsController | null { + return editor.getContribution(InlineEditsController.ID); + } + + private readonly _enabled = observableConfigValue('editor.inlineEdits.enabled', false, this._configurationService); + private readonly _editorObs = observableCodeEditor(this.editor); + private readonly _selection = derived(this, reader => this._editorObs.cursorSelection.read(reader) ?? new Selection(1, 1, 1, 1)); + + private readonly _debounceValue = this._debounceService.for( + this._languageFeaturesService.inlineCompletionsProvider, + 'InlineEditsDebounce', + { min: 50, max: 50 } + ); + + public readonly model = derivedDisposable(this, reader => { + if (!this._enabled.read(reader)) { + return undefined; + } + if (this._editorObs.isReadonly.read(reader)) { return undefined; } + const textModel = this._editorObs.model.read(reader); + if (!textModel) { return undefined; } + + const model: InlineEditsModel = this._instantiationService.createInstance( + readHotReloadableExport(InlineEditsModel, reader), + textModel, + this._editorObs.versionId, + this._selection, + this._debounceValue, + ); + + return model; + }); + + private readonly _hadInlineEdit = derivedObservableWithCache(this, (reader, lastValue) => lastValue || this.model.read(reader)?.inlineEdit.read(reader) !== undefined); + + protected readonly _widget = derivedDisposable(this, reader => { + if (!this._hadInlineEdit.read(reader)) { return undefined; } + + return this._instantiationService.createInstance( + readHotReloadableExport(InlineEditsWidget, reader), + this.editor, + this.model.map((m, reader) => m?.inlineEdit.read(reader)), + flattenSettableObservable((reader) => this.model.read(reader)?.userPrompt ?? observableValue('empty', '')), + ); + }); + + constructor( + public readonly editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ILanguageFeatureDebounceService private readonly _debounceService: ILanguageFeatureDebounceService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(); + + this._register(bindContextKey(inlineEditVisible, this._contextKeyService, r => !!this.model.read(r)?.inlineEdit.read(r))); + this._register(bindContextKey(isPinnedContextKey, this._contextKeyService, r => !!this.model.read(r)?.isPinned.read(r))); + + this.model.recomputeInitiallyAndOnChange(this._store); + this._widget.recomputeInitiallyAndOnChange(this._store); + } +} + +function flattenSettableObservable(fn: (reader: IReader | undefined) => ISettableObservable): ISettableObservable { + return derivedWithSetter(undefined, reader => { + const obs = fn(reader); + return obs.read(reader); + }, (value, tx) => { + fn(undefined).set(value, tx); + }); +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts new file mode 100644 index 00000000000..812818c85b1 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts @@ -0,0 +1,289 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { timeout } from 'vs/base/common/async'; +import { CancellationToken, cancelOnDispose } from 'vs/base/common/cancellation'; +import { itemsEquals, structuralEquals } from 'vs/base/common/equals'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ISettableObservable, ITransaction, ObservablePromise, derived, derivedHandleChanges, derivedOpts, disposableObservableValue, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction } from 'vs/base/common/observable'; +import { derivedDisposable } from 'vs/base/common/observableInternal/derived'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { Command, InlineCompletionContext, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { IFeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { IModelService } from 'vs/editor/common/services/model'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; +import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from 'vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions'; +import { InlineEdit } from 'vs/editor/contrib/inlineEdits/browser/inlineEditsWidget'; + +export class InlineEditsModel extends Disposable { + private static _modelId = 0; + private static _createUniqueUri(): URI { + return URI.from({ scheme: 'inline-edits', path: new Date().toString() + String(InlineEditsModel._modelId++) }); + } + + private readonly _forceUpdateExplicitlySignal = observableSignal(this); + + // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. + private readonly _selectedInlineCompletionId = observableValue(this, undefined); + + private readonly _isActive = observableValue(this, false); + + private readonly _originalModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditsModel._createUniqueUri())).keepObserved(this._store); + private readonly _modifiedModel = derivedDisposable(() => this._modelService.createModel('', null, InlineEditsModel._createUniqueUri())).keepObserved(this._store); + + private readonly _pinnedRange = new TrackedRange(this.textModel, this._textModelVersionId); + + public readonly isPinned = this._pinnedRange.range.map(range => !!range); + + public readonly userPrompt: ISettableObservable = observableValue(this, undefined); + + constructor( + public readonly textModel: ITextModel, + public readonly _textModelVersionId: IObservable, + private readonly _selection: IObservable, + protected readonly _debounceValue: IFeatureDebounceInformation, + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService, + @IModelService private readonly _modelService: IModelService, + ) { + super(); + + this._register(recomputeInitiallyAndOnChange(this._fetchInlineEditsPromise)); + } + + public readonly inlineEdit = derived(this, reader => { + return this._inlineEdit.read(reader)?.promiseResult.read(reader)?.data; + }); + + public readonly _inlineEdit = derived | undefined>(this, reader => { + const edit = this.selectedInlineEdit.read(reader); + if (!edit) { return undefined; } + const range = edit.inlineCompletion.range; + if (edit.inlineCompletion.insertText.trim() === '') { + return undefined; + } + + let newLines = edit.inlineCompletion.insertText.split(/\r\n|\r|\n/); + + function removeIndentation(lines: string[]): string[] { + const indentation = lines[0].match(/^\s*/)?.[0] ?? ''; + return lines.map(l => l.replace(new RegExp('^' + indentation), '')); + } + newLines = removeIndentation(newLines); + + const existing = this.textModel.getValueInRange(range); + let existingLines = existing.split(/\r\n|\r|\n/); + existingLines = removeIndentation(existingLines); + this._originalModel.get().setValue(existingLines.join('\n')); + this._modifiedModel.get().setValue(newLines.join('\n')); + + const d = this._diffProviderFactoryService.createDiffProvider({ diffAlgorithm: 'advanced' }); + return ObservablePromise.fromFn(async () => { + const result = await d.computeDiff(this._originalModel.get(), this._modifiedModel.get(), { + computeMoves: false, + ignoreTrimWhitespace: false, + maxComputationTimeMs: 1000, + }, CancellationToken.None); + + if (result.identical) { + return undefined; + } + + return new InlineEdit(LineRange.fromRangeInclusive(range), removeIndentation(newLines), result.changes); + }); + }); + + private readonly _fetchStore = this._register(new DisposableStore()); + + private readonly _inlineEditsFetchResult = disposableObservableValue(this, undefined); + private readonly _inlineEdits = derivedOpts({ owner: this, equalsFn: structuralEquals }, reader => { + return this._inlineEditsFetchResult.read(reader)?.completions.map(c => new InlineEditData(c)) ?? []; + }); + + private readonly _fetchInlineEditsPromise = derivedHandleChanges({ + owner: this, + createEmptyChangeSummary: () => ({ + inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic + }), + handleChange: (ctx, changeSummary) => { + /** @description fetch inline completions */ + if (ctx.didChange(this._forceUpdateExplicitlySignal)) { + changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; + } + return true; + }, + }, async (reader, changeSummary) => { + this._fetchStore.clear(); + this._forceUpdateExplicitlySignal.read(reader); + /*if (!this._isActive.read(reader)) { + return undefined; + }*/ + this._textModelVersionId.read(reader); + + function mapValue(value: T, fn: (value: T) => TOut): TOut { + return fn(value); + } + + const selection = this._pinnedRange.range.read(reader) ?? mapValue(this._selection.read(reader), v => v.isEmpty() ? undefined : v); + if (!selection) { + this._inlineEditsFetchResult.set(undefined, undefined); + this.userPrompt.set(undefined, undefined); + return undefined; + } + const context: InlineCompletionContext = { + triggerKind: changeSummary.inlineCompletionTriggerKind, + selectedSuggestionInfo: undefined, + userPrompt: this.userPrompt.read(reader), + }; + + const token = cancelOnDispose(this._fetchStore); + await timeout(200, token); + const result = await provideInlineCompletions(this.languageFeaturesService.inlineCompletionsProvider, selection, this.textModel, context, token); + if (token.isCancellationRequested) { + return; + } + + this._inlineEditsFetchResult.set(result, undefined); + }); + + public async trigger(tx?: ITransaction): Promise { + this._isActive.set(true, tx); + await this._fetchInlineEditsPromise.get(); + } + + public async triggerExplicitly(tx?: ITransaction): Promise { + subtransaction(tx, tx => { + this._isActive.set(true, tx); + this._forceUpdateExplicitlySignal.trigger(tx); + }); + await this._fetchInlineEditsPromise.get(); + } + + public stop(tx?: ITransaction): void { + subtransaction(tx, tx => { + this.userPrompt.set(undefined, tx); + this._isActive.set(false, tx); + this._inlineEditsFetchResult.set(undefined, tx); + this._pinnedRange.setRange(undefined, tx); + //this._source.clear(tx); + }); + } + + private readonly _filteredInlineEditItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => { + return this._inlineEdits.read(reader); + }); + + public readonly selectedInlineCompletionIndex = derived(this, (reader) => { + const selectedInlineCompletionId = this._selectedInlineCompletionId.read(reader); + const filteredCompletions = this._filteredInlineEditItems.read(reader); + const idx = this._selectedInlineCompletionId === undefined ? -1 + : filteredCompletions.findIndex(v => v.semanticId === selectedInlineCompletionId); + if (idx === -1) { + // Reset the selection so that the selection does not jump back when it appears again + this._selectedInlineCompletionId.set(undefined, undefined); + return 0; + } + return idx; + }); + + public readonly selectedInlineEdit = derived(this, (reader) => { + const filteredCompletions = this._filteredInlineEditItems.read(reader); + const idx = this.selectedInlineCompletionIndex.read(reader); + return filteredCompletions[idx]; + }); + + public readonly activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, + r => this.selectedInlineEdit.read(r)?.inlineCompletion.source.inlineCompletions.commands ?? [] + ); + + private async _deltaSelectedInlineCompletionIndex(delta: 1 | -1): Promise { + await this.triggerExplicitly(); + + const completions = this._filteredInlineEditItems.get() || []; + if (completions.length > 0) { + const newIdx = (this.selectedInlineCompletionIndex.get() + delta + completions.length) % completions.length; + this._selectedInlineCompletionId.set(completions[newIdx].semanticId, undefined); + } else { + this._selectedInlineCompletionId.set(undefined, undefined); + } + } + + public async next(): Promise { + await this._deltaSelectedInlineCompletionIndex(1); + } + + public async previous(): Promise { + await this._deltaSelectedInlineCompletionIndex(-1); + } + + public togglePin(): void { + if (this.isPinned.get()) { + this._pinnedRange.setRange(undefined, undefined); + } else { + this._pinnedRange.setRange(this._selection.get(), undefined); + } + } + + public async accept(editor: ICodeEditor): Promise { + if (editor.getModel() !== this.textModel) { + throw new BugIndicatingError(); + } + const edit = this.selectedInlineEdit.get(); + if (!edit) { + return; + } + + editor.pushUndoStop(); + editor.executeEdits( + 'inlineSuggestion.accept', + [ + edit.inlineCompletion.toSingleTextEdit().toSingleEditOperation() + ] + ); + this.stop(); + } +} + +class InlineEditData { + public readonly semanticId = this.inlineCompletion.hash(); + + constructor(public readonly inlineCompletion: InlineCompletionItem) { + + } +} + +class TrackedRange extends Disposable { + private readonly _decorations = observableValue(this, []); + + constructor( + private readonly _textModel: ITextModel, + private readonly _versionId: IObservable, + ) { + super(); + this._register(toDisposable(() => { + this._textModel.deltaDecorations(this._decorations.get(), []); + })); + } + + setRange(range: Range | undefined, tx: ITransaction | undefined): void { + this._decorations.set(this._textModel.deltaDecorations(this._decorations.get(), range ? [{ range, options: { description: 'trackedRange' } }] : []), tx); + } + + public readonly range = derived(this, reader => { + this._versionId.read(reader); + const deco = this._decorations.read(reader)[0]; + if (!deco) { return null; } + + return this._textModel.getDecorationRange(deco) ?? null; + }); +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.css b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.css new file mode 100644 index 00000000000..68910c883a6 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.css @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor div.inline-edits-widget { + --widget-color: var(--vscode-notifications-background); + + .promptEditor .monaco-editor { + --vscode-editor-placeholder-foreground: var(--vscode-editorGhostText-foreground); + } + + .toolbar, .promptEditor { + opacity: 0; + transition: opacity 0.2s ease-in-out; + } + &:hover, &.focused { + .toolbar, .promptEditor { + opacity: 1; + } + } + + .preview .monaco-editor { + + .mtk1 { + /*color: rgba(215, 215, 215, 0.452);*/ + color: var(--vscode-editorGhostText-foreground); + } + .view-overlays .current-line-exact { + border: none; + } + + .current-line-margin { + border: none; + } + + --vscode-editor-background: var(--widget-color); + } + + svg { + .gradient-start { + stop-color: var(--vscode-editor-background); + } + + .gradient-stop { + stop-color: var(--widget-color); + } + } +} diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts new file mode 100644 index 00000000000..331520cffe0 --- /dev/null +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts @@ -0,0 +1,400 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h, svgElem } from 'vs/base/browser/dom'; +import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { autorun, constObservable, derived, IObservable, ISettableObservable } from 'vs/base/common/observable'; +import { derivedWithSetter } from 'vs/base/common/observableInternal/derived'; +import 'vs/css!./inlineEditsWidget'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecoration, diffDeleteDecorationEmpty, diffLineAddDecorationBackgroundWithIndicator, diffLineDeleteDecorationBackgroundWithIndicator, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditor/registrations.contribution'; +import { appendRemoveOnDispose, applyStyle } from 'vs/editor/browser/widget/diffEditor/utils'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; +import { PlaceholderTextContribution } from '../../placeholderText/browser/placeholderTextContribution'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +export class InlineEdit { + constructor( + public readonly range: LineRange, + public readonly newLines: string[], + public readonly changes: readonly DetailedLineRangeMapping[], + ) { + + } +} + +export class InlineEditsWidget extends Disposable { + private readonly _editorObs = observableCodeEditor(this._editor); + + private readonly _elements = h('div.inline-edits-widget', { + style: { + position: 'absolute', + overflow: 'visible', + top: '0px', + left: '0px', + }, + }, [ + h('div@editorContainer', { style: { position: 'absolute', top: '0px', left: '0px', width: '500px', height: '500px', } }, [ + h('div.toolbar@toolbar', { style: { position: 'absolute', top: '-25px', left: '0px' } }), + h('div.promptEditor@promptEditor', { style: { position: 'absolute', top: '-25px', left: '80px', width: '300px', height: '22px' } }), + h('div.preview@editor', { style: { position: 'absolute', top: '0px', left: '0px' } }), + ]), + svgElem('svg', { style: { overflow: 'visible', pointerEvents: 'none' }, }, [ + svgElem('defs', [ + svgElem('linearGradient', { + id: 'Gradient2', + x1: '0', + y1: '0', + x2: '1', + y2: '0', + }, [ + /*svgElem('stop', { offset: '0%', class: 'gradient-start', }), + svgElem('stop', { offset: '0%', class: 'gradient-start', }), + svgElem('stop', { offset: '20%', class: 'gradient-stop', }),*/ + svgElem('stop', { offset: '0%', class: 'gradient-stop', }), + svgElem('stop', { offset: '100%', class: 'gradient-stop', }), + ]), + ]), + svgElem('path@path', { + d: '', + fill: 'url(#Gradient2)', + }), + ]), + ]); + + protected readonly _toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar, MenuId.InlineEditsActions, { + toolbarOptions: { + primaryGroup: g => g.startsWith('primary'), + }, + })); + private readonly _previewTextModel = this._register(this._instantiationService.createInstance( + TextModel, + '', + PLAINTEXT_LANGUAGE_ID, + TextModel.DEFAULT_CREATION_OPTIONS, + null + )); + + private readonly _setText = derived(reader => { + const edit = this._edit.read(reader); + if (!edit) { return; } + this._previewTextModel.setValue(edit.newLines.join('\n')); + }).recomputeInitiallyAndOnChange(this._store); + + + private readonly _promptTextModel = this._register(this._instantiationService.createInstance( + TextModel, + '', + PLAINTEXT_LANGUAGE_ID, + TextModel.DEFAULT_CREATION_OPTIONS, + null + )); + private readonly _promptEditor = this._register(this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this._elements.promptEditor, + { + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + guides: { + indentation: false, + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveIndentation: false, + }, + folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + columnSelection: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + placeholder: 'Describe the change you want...', + fontFamily: DEFAULT_FONT_FAMILY, + }, + { + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + SuggestController.ID, + PlaceholderTextContribution.ID, + ContextMenuController.ID, + ]), + isSimpleWidget: true + }, + this._editor + )); + + private readonly _previewEditor = this._register(this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this._elements.editor, + { + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + guides: { + indentation: false, + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveIndentation: false, + }, + folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + columnSelection: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + }, + { contributions: [], }, + this._editor + )); + + private readonly _previewEditorObs = observableCodeEditor(this._previewEditor); + + private readonly _decorations = derived(this, (reader) => { + this._setText.read(reader); + const diff = this._edit.read(reader)?.changes; + if (!diff) { return []; } + + const originalDecorations: IModelDeltaDecoration[] = []; + const modifiedDecorations: IModelDeltaDecoration[] = []; + + if (diff.length === 1 && diff[0].innerChanges![0].modifiedRange.equalsRange(this._previewTextModel.getFullModelRange())) { + return []; + } + + for (const m of diff) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffLineDeleteDecorationBackgroundWithIndicator }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffLineAddDecorationBackgroundWithIndicator }); + } + + if (m.modified.isEmpty || m.original.isEmpty) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffWholeLineDeleteDecoration }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration }); + } + } else { + for (const i of m.innerChanges || []) { + // Don't show empty markers outside the line range + if (m.original.contains(i.originalRange.startLineNumber)) { + originalDecorations.push({ range: i.originalRange, options: i.originalRange.isEmpty() ? diffDeleteDecorationEmpty : diffDeleteDecoration }); + } + if (m.modified.contains(i.modifiedRange.startLineNumber)) { + modifiedDecorations.push({ range: i.modifiedRange, options: i.modifiedRange.isEmpty() ? diffAddDecorationEmpty : diffAddDecoration }); + } + } + } + } + + return modifiedDecorations; + }); + + private readonly _layout1 = derived(this, reader => { + const model = this._editor.getModel()!; + const inlineEdit = this._edit.read(reader); + if (!inlineEdit) { return null; } + + const range = inlineEdit.range; + + let maxLeft = 0; + for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) { + const column = model.getLineMaxColumn(i); + const left = this._editor.getOffsetForColumn(i, column); + maxLeft = Math.max(maxLeft, left); + } + + const layoutInfo = this._editor.getLayoutInfo(); + const contentLeft = layoutInfo.contentLeft; + + return { left: contentLeft + maxLeft }; + }); + + private readonly _layout = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + if (!inlineEdit) { return null; } + + const range = inlineEdit.range; + + const scrollLeft = this._editorObs.scrollLeft.read(reader); + + const left = this._layout1.read(reader)!.left + 20 - scrollLeft; + + const selectionTop = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); + const selectionBottom = this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); + + const topCode = new Point(left, selectionTop); + const bottomCode = new Point(left, selectionBottom); + const codeHeight = selectionBottom - selectionTop; + + const codeEditDist = 50; + const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.newLines.length; + const difference = codeHeight - editHeight; + const topEdit = new Point(left + codeEditDist, selectionTop + (difference / 2)); + const bottomEdit = new Point(left + codeEditDist, selectionBottom - (difference / 2)); + + return { + topCode, + bottomCode, + codeHeight, + topEdit, + bottomEdit, + editHeight, + }; + }); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _edit: IObservable, + private readonly _userPrompt: ISettableObservable, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + const visible = derived(this, reader => this._edit.read(reader) !== undefined || this._userPrompt.read(reader) !== undefined); + this._register(applyStyle(this._elements.root, { + display: derived(this, reader => visible.read(reader) ? 'block' : 'none') + })); + + this._register(appendRemoveOnDispose(this._editor.getDomNode()!, this._elements.root)); + + this._register(observableCodeEditor(_editor).createOverlayWidget({ + domNode: this._elements.root, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: derived(reader => { + const x = this._layout1.read(reader)?.left; + if (x === undefined) { return 0; } + const width = this._previewEditorObs.contentWidth.read(reader); + return x + width; + }), + })); + + this._previewEditor.setModel(this._previewTextModel); + + this._register(this._previewEditorObs.setDecorations(this._decorations)); + + this._register(autorun(reader => { + const layoutInfo = this._layout.read(reader); + if (!layoutInfo) { return; } + + const { topCode, bottomCode, topEdit, bottomEdit, editHeight } = layoutInfo; + + const straightWidthCode = 10; + const straightWidthEdit = 0; + const bezierDist = 40; + + const path = new PathBuilder() + .moveTo(topCode) + .lineTo(topCode.deltaX(straightWidthCode)) + .curveTo( + topCode.deltaX(straightWidthCode + bezierDist), + topEdit.deltaX(-bezierDist - straightWidthEdit), + topEdit.deltaX(-straightWidthEdit), + ) + .lineTo(topEdit) + .lineTo(bottomEdit) + .lineTo(bottomEdit.deltaX(-straightWidthEdit)) + .curveTo( + bottomEdit.deltaX(-bezierDist - straightWidthEdit), + bottomCode.deltaX(straightWidthCode + bezierDist), + bottomCode.deltaX(straightWidthCode), + ) + .lineTo(bottomCode) + .build(); + + + this._elements.path.setAttribute('d', path); + + this._elements.editorContainer.style.top = `${topEdit.y}px`; + this._elements.editorContainer.style.left = `${topEdit.x}px`; + this._elements.editorContainer.style.height = `${editHeight}px`; + + const width = this._previewEditorObs.contentWidth.read(reader); + this._previewEditor.layout({ height: editHeight, width }); + })); + + this._promptEditor.setModel(this._promptTextModel); + this._promptEditor.layout(); + this._register(createTwoWaySync(mapSettableObservable(this._userPrompt, v => v ?? '', v => v), observableCodeEditor(this._promptEditor).value)); + + this._register(autorun(reader => { + const isFocused = observableCodeEditor(this._promptEditor).isFocused.read(reader); + this._elements.root.classList.toggle('focused', isFocused); + })); + } +} + +function mapSettableObservable(obs: ISettableObservable, fn1: (value: T) => T1, fn2: (value: T1) => T): ISettableObservable { + return derivedWithSetter(undefined, reader => fn1(obs.read(reader)), (value, tx) => obs.set(fn2(value), tx)); +} + +class Point { + constructor( + public readonly x: number, + public readonly y: number, + ) { } + + public add(other: Point): Point { + return new Point(this.x + other.x, this.y + other.y); + } + + public deltaX(delta: number): Point { + return new Point(this.x + delta, this.y); + } +} + +class PathBuilder { + private _data: string = ''; + + public moveTo(point: Point): this { + this._data += `M ${point.x} ${point.y} `; + return this; + } + + public lineTo(point: Point): this { + this._data += `L ${point.x} ${point.y} `; + return this; + } + + public curveTo(cp1: Point, cp2: Point, to: Point): this { + this._data += `C ${cp1.x} ${cp1.y} ${cp2.x} ${cp2.y} ${to.x} ${to.y} `; + return this; + } + + public build(): string { + return this._data; + } +} + +function createTwoWaySync(main: ISettableObservable, target: ISettableObservable): IDisposable { + const store = new DisposableStore(); + store.add(autorun(reader => { + const value = main.read(reader); + target.set(value, undefined); + })); + store.add(autorun(reader => { + const value = target.read(reader); + main.set(value, undefined); + })); + return store; +} diff --git a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts index db892ea676c..25f273c1262 100644 --- a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts +++ b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { CancelablePromise, disposableTimeout } from 'vs/base/common/async'; +import { disposableTimeout } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { noBreakWhitespace } from 'vs/base/common/strings'; @@ -114,13 +114,13 @@ export class InlineProgressManager extends Disposable { private readonly _showPromise = this._register(new MutableDisposable()); private readonly _currentDecorations: IEditorDecorationsCollection; - private readonly _currentWidget = new MutableDisposable(); + private readonly _currentWidget = this._register(new MutableDisposable()); private _operationIdPool = 0; private _currentOperation?: number; constructor( - readonly id: string, + private readonly id: string, private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { @@ -129,7 +129,12 @@ export class InlineProgressManager extends Disposable { this._currentDecorations = _editor.createDecorationsCollection(); } - public async showWhile(position: IPosition, title: string, promise: CancelablePromise): Promise { + public override dispose(): void { + super.dispose(); + this._currentDecorations.clear(); + } + + public async showWhile(position: IPosition, title: string, promise: Promise, delegate: InlineProgressDelegate, delayOverride?: number): Promise { const operationId = this._operationIdPool++; this._currentOperation = operationId; @@ -143,9 +148,9 @@ export class InlineProgressManager extends Disposable { }]); if (decorationIds.length > 0) { - this._currentWidget.value = this._instantiationService.createInstance(InlineProgressWidget, this.id, this._editor, range, title, promise); + this._currentWidget.value = this._instantiationService.createInstance(InlineProgressWidget, this.id, this._editor, range, title, delegate); } - }, this._showDelay); + }, delayOverride ?? this._showDelay); try { return await promise; diff --git a/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts b/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts index 4ef8e7d2cfc..17b47434db7 100644 --- a/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts +++ b/src/vs/editor/contrib/lineSelection/test/browser/lineSelection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction } from 'vs/editor/browser/editorExtensions'; diff --git a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts index 45b3fafba71..60601d1fcdd 100644 --- a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts @@ -11,6 +11,7 @@ import { ReplaceCommand, ReplaceCommandThatPreservesSelection, ReplaceCommandTha import { TrimTrailingWhitespaceCommand } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations'; +import { EnterOperation } from 'vs/editor/common/cursor/cursorTypeEditOperations'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -594,7 +595,7 @@ export class InsertLineBeforeAction extends EditorAction { return; } editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineInsertBefore(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); + editor.executeCommands(this.id, EnterOperation.lineInsertBefore(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); } } @@ -619,7 +620,7 @@ export class InsertLineAfterAction extends EditorAction { return; } editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineInsertAfter(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); + editor.executeCommands(this.id, EnterOperation.lineInsertAfter(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); } } diff --git a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts index 68614a2f432..9a4f6800938 100644 --- a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts @@ -42,6 +42,13 @@ export class MoveLinesCommand implements ICommand { public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { + const getLanguageId = () => { + return model.getLanguageId(); + }; + const getLanguageIdAtPosition = (lineNumber: number, column: number) => { + return model.getLanguageIdAtPosition(lineNumber, column); + }; + const modelLineCount = model.getLineCount(); if (this._isMovingDown && this._selection.endLineNumber === modelLineCount) { @@ -63,20 +70,6 @@ export class MoveLinesCommand implements ICommand { const { tabSize, indentSize, insertSpaces } = model.getOptions(); const indentConverter = this.buildIndentConverter(tabSize, indentSize, insertSpaces); - const virtualModel: IVirtualModel = { - tokenization: { - getLineTokens: (lineNumber: number) => { - return model.tokenization.getLineTokens(lineNumber); - }, - getLanguageId: () => { - return model.getLanguageId(); - }, - getLanguageIdAtPosition: (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); - }, - }, - getLineContent: null as unknown as (lineNumber: number) => string, - }; if (s.startLineNumber === s.endLineNumber && model.getLineMaxColumn(s.startLineNumber) === 1) { // Current line is empty @@ -120,12 +113,25 @@ export class MoveLinesCommand implements ICommand { insertingText = newIndentation + this.trimStart(movingLineText); } else { // no enter rule matches, let's check indentatin rules then. - virtualModel.getLineContent = (lineNumber: number) => { - if (lineNumber === s.startLineNumber) { - return model.getLineContent(movingLineNumber); - } else { - return model.getLineContent(lineNumber); - } + const virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + return model.tokenization.getLineTokens(movingLineNumber); + } else { + return model.tokenization.getLineTokens(lineNumber); + } + }, + getLanguageId, + getLanguageIdAtPosition, + }, + getLineContent: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + return model.getLineContent(movingLineNumber); + } else { + return model.getLineContent(lineNumber); + } + }, }; const indentOfMovingLine = getGoodIndentForLine( this._autoIndent, @@ -159,14 +165,30 @@ export class MoveLinesCommand implements ICommand { } } else { // it doesn't match onEnter rules, let's check indentation rules then. - virtualModel.getLineContent = (lineNumber: number) => { - if (lineNumber === s.startLineNumber) { - return insertingText; - } else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) { - return model.getLineContent(lineNumber - 1); - } else { - return model.getLineContent(lineNumber); - } + const virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + // TODO@aiday-mar: the tokens here don't correspond exactly to the corresponding content (after indentation adjustment), have to fix this. + return model.tokenization.getLineTokens(movingLineNumber); + } else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) { + return model.tokenization.getLineTokens(lineNumber - 1); + } else { + return model.tokenization.getLineTokens(lineNumber); + } + }, + getLanguageId, + getLanguageIdAtPosition, + }, + getLineContent: (lineNumber: number) => { + if (lineNumber === s.startLineNumber) { + return insertingText; + } else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) { + return model.getLineContent(lineNumber - 1); + } else { + return model.getLineContent(lineNumber); + } + }, }; const newIndentatOfMovingBlock = getGoodIndentForLine( @@ -204,12 +226,25 @@ export class MoveLinesCommand implements ICommand { builder.addEditOperation(new Range(s.endLineNumber, model.getLineMaxColumn(s.endLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)), '\n' + movingLineText); if (this.shouldAutoIndent(model, s)) { - virtualModel.getLineContent = (lineNumber: number) => { - if (lineNumber === movingLineNumber) { - return model.getLineContent(s.startLineNumber); - } else { - return model.getLineContent(lineNumber); - } + const virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number) => { + if (lineNumber === movingLineNumber) { + return model.tokenization.getLineTokens(s.startLineNumber); + } else { + return model.tokenization.getLineTokens(lineNumber); + } + }, + getLanguageId, + getLanguageIdAtPosition, + }, + getLineContent: (lineNumber: number) => { + if (lineNumber === movingLineNumber) { + return model.getLineContent(s.startLineNumber); + } else { + return model.getLineContent(lineNumber); + } + }, }; const ret = this.matchEnterRule(model, indentConverter, tabSize, s.startLineNumber, s.startLineNumber - 2); diff --git a/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts index c53496fb5de..48348ae9690 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Selection } from 'vs/editor/common/core/selection'; import { CopyLinesCommand } from 'vs/editor/contrib/linesOperations/browser/copyLinesCommand'; diff --git a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts index 5425697a2e4..f99fb62422d 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts b/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts index 982dc07426b..80d6ed93730 100644 --- a/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts +++ b/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts b/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts index 64de23fce27..008150e7c09 100644 --- a/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts +++ b/src/vs/editor/contrib/multicursor/test/browser/multicursor.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts index f5b8af528e3..3036277c448 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts @@ -387,4 +387,4 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } } -registerColor('editorHoverWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hcDark: listHighlightForeground, hcLight: listHighlightForeground }, nls.localize('editorHoverWidgetHighlightForeground', 'Foreground color of the active item in the parameter hint.')); +registerColor('editorHoverWidget.highlightForeground', listHighlightForeground, nls.localize('editorHoverWidgetHighlightForeground', 'Foreground color of the active item in the parameter hint.')); diff --git a/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts b/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts index 6c84215bc72..10372b408c4 100644 --- a/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts +++ b/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { promiseWithResolvers } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/peekView/browser/peekView.ts b/src/vs/editor/contrib/peekView/browser/peekView.ts index a0f2dfd914e..86f6cb1d47f 100644 --- a/src/vs/editor/contrib/peekView/browser/peekView.ts +++ b/src/vs/editor/contrib/peekView/browser/peekView.ts @@ -289,8 +289,8 @@ export const peekViewResultsFileForeground = registerColor('peekViewResult.fileF export const peekViewResultsSelectionBackground = registerColor('peekViewResult.selectionBackground', { dark: '#3399ff33', light: '#3399ff33', hcDark: null, hcLight: null }, nls.localize('peekViewResultsSelectionBackground', 'Background color of the selected entry in the peek view result list.')); export const peekViewResultsSelectionForeground = registerColor('peekViewResult.selectionForeground', { dark: Color.white, light: '#6C6C6C', hcDark: Color.white, hcLight: editorForeground }, nls.localize('peekViewResultsSelectionForeground', 'Foreground color of the selected entry in the peek view result list.')); export const peekViewEditorBackground = registerColor('peekViewEditor.background', { dark: '#001F33', light: '#F2F8FC', hcDark: Color.black, hcLight: Color.white }, nls.localize('peekViewEditorBackground', 'Background color of the peek view editor.')); -export const peekViewEditorGutterBackground = registerColor('peekViewEditorGutter.background', { dark: peekViewEditorBackground, light: peekViewEditorBackground, hcDark: peekViewEditorBackground, hcLight: peekViewEditorBackground }, nls.localize('peekViewEditorGutterBackground', 'Background color of the gutter in the peek view editor.')); -export const peekViewEditorStickyScrollBackground = registerColor('peekViewEditorStickyScroll.background', { dark: peekViewEditorBackground, light: peekViewEditorBackground, hcDark: peekViewEditorBackground, hcLight: peekViewEditorBackground }, nls.localize('peekViewEditorStickScrollBackground', 'Background color of sticky scroll in the peek view editor.')); +export const peekViewEditorGutterBackground = registerColor('peekViewEditorGutter.background', peekViewEditorBackground, nls.localize('peekViewEditorGutterBackground', 'Background color of the gutter in the peek view editor.')); +export const peekViewEditorStickyScrollBackground = registerColor('peekViewEditorStickyScroll.background', peekViewEditorBackground, nls.localize('peekViewEditorStickScrollBackground', 'Background color of sticky scroll in the peek view editor.')); export const peekViewResultsMatchHighlight = registerColor('peekViewResult.matchHighlightBackground', { dark: '#ea5c004d', light: '#ea5c004d', hcDark: null, hcLight: null }, nls.localize('peekViewResultsMatchHighlight', 'Match highlight color in the peek view result list.')); export const peekViewEditorMatchHighlight = registerColor('peekViewEditor.matchHighlightBackground', { dark: '#ff8f0099', light: '#f5d802de', hcDark: null, hcLight: null }, nls.localize('peekViewEditorMatchHighlight', 'Match highlight color in the peek view editor.')); diff --git a/src/vs/editor/contrib/placeholderText/browser/placeholderText.contribution.ts b/src/vs/editor/contrib/placeholderText/browser/placeholderText.contribution.ts new file mode 100644 index 00000000000..e9994c4b01c --- /dev/null +++ b/src/vs/editor/contrib/placeholderText/browser/placeholderText.contribution.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./placeholderText'; +import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { ghostTextForeground } from 'vs/editor/common/core/editorColorRegistry'; +import { localize } from 'vs/nls'; +import { registerColor } from 'vs/platform/theme/common/colorUtils'; +import { PlaceholderTextContribution } from './placeholderTextContribution'; +import { wrapInReloadableClass1 } from 'vs/platform/observable/common/wrapInReloadableClass'; + +registerEditorContribution(PlaceholderTextContribution.ID, wrapInReloadableClass1(() => PlaceholderTextContribution), EditorContributionInstantiation.Eager); + +registerColor('editor.placeholder.foreground', ghostTextForeground, localize('placeholderForeground', 'Foreground color of the placeholder text in the editor.')); diff --git a/src/vs/editor/contrib/placeholderText/browser/placeholderText.css b/src/vs/editor/contrib/placeholderText/browser/placeholderText.css index c42b21a3ba2..043b6f15632 100644 --- a/src/vs/editor/contrib/placeholderText/browser/placeholderText.css +++ b/src/vs/editor/contrib/placeholderText/browser/placeholderText.css @@ -3,6 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-editor .placeholder-text { - color: var(--vscode-editorGhostText-foreground) !important; +.monaco-editor { + --vscode-editor-placeholder-foreground: var(--vscode-editorGhostText-foreground); + + .editorPlaceholder { + top: 0px; + position: absolute; + overflow: hidden; + text-overflow: ellipsis; + text-wrap: nowrap; + pointer-events: none; + + color: var(--vscode-editor-placeholder-foreground); + } } diff --git a/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts b/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts index 52c0edd1cb8..f048991d0f9 100644 --- a/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts +++ b/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts @@ -3,64 +3,80 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { h } from 'vs/base/browser/dom'; import { structuralEquals } from 'vs/base/common/equals'; import { Disposable } from 'vs/base/common/lifecycle'; -import { derived, derivedOpts, observableValue } from 'vs/base/common/observable'; -import 'vs/css!./placeholderText'; +import { autorun, constObservable, derivedObservableWithCache, derivedOpts, IObservable, IReader } from 'vs/base/common/observable'; +import { DebugOwner } from 'vs/base/common/observableInternal/debugName'; +import { derivedWithStore } from 'vs/base/common/observableInternal/derived'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { obsCodeEditor } from 'vs/editor/browser/observableUtilities'; -import { Range } from 'vs/editor/common/core/range'; +import { observableCodeEditor } from 'vs/editor/browser/observableCodeEditor'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { IModelDeltaDecoration, InjectedTextCursorStops } from 'vs/editor/common/model'; +/** + * Use the editor option to set the placeholder text. +*/ export class PlaceholderTextContribution extends Disposable implements IEditorContribution { public static get(editor: ICodeEditor): PlaceholderTextContribution { return editor.getContribution(PlaceholderTextContribution.ID)!; } public static readonly ID = 'editor.contrib.placeholderText'; - private readonly _editorObs = obsCodeEditor(this._editor); + private readonly _editorObs = observableCodeEditor(this._editor); - private readonly _placeholderText = observableValue(this, undefined); + private readonly _placeholderText = this._editorObs.getOption(EditorOption.placeholder); - private readonly _decorationOptions = derivedOpts<{ placeholder: string } | undefined>({ owner: this, equalsFn: structuralEquals }, reader => { + private readonly _state = derivedOpts<{ placeholder: string } | undefined>({ owner: this, equalsFn: structuralEquals }, reader => { const p = this._placeholderText.read(reader); if (!p) { return undefined; } if (!this._editorObs.valueIsEmpty.read(reader)) { return undefined; } - return { placeholder: p }; }); - private readonly _decorations = derived(this, (reader) => { - const options = this._decorationOptions.read(reader); - if (!options) { return []; } + private readonly _shouldViewBeAlive = isOrWasTrue(this, reader => this._state.read(reader)?.placeholder !== undefined); - return [{ - range: new Range(1, 1, 1, 1), - options: { - description: 'placeholder', - showIfCollapsed: true, - after: { - content: options.placeholder, - cursorStops: InjectedTextCursorStops.None, - inlineClassName: 'placeholder-text' - } - } - }]; + private readonly _view = derivedWithStore((reader, store) => { + if (!this._shouldViewBeAlive.read(reader)) { return; } + + const element = h('div.editorPlaceholder'); + + store.add(autorun(reader => { + const data = this._state.read(reader); + const shouldBeVisibile = data?.placeholder !== undefined; + element.root.style.display = shouldBeVisibile ? 'block' : 'none'; + element.root.innerText = data?.placeholder ?? ''; + })); + store.add(autorun(reader => { + const info = this._editorObs.layoutInfo.read(reader); + element.root.style.left = `${info.contentLeft}px`; + element.root.style.width = (info.contentWidth - info.verticalScrollbarWidth) + 'px'; + element.root.style.top = `${this._editor.getTopForLineNumber(0)}px`; + })); + store.add(autorun(reader => { + element.root.style.fontFamily = this._editorObs.getOption(EditorOption.fontFamily).read(reader); + element.root.style.fontSize = this._editorObs.getOption(EditorOption.fontSize).read(reader) + 'px'; + element.root.style.lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader) + 'px'; + })); + store.add(this._editorObs.createOverlayWidget({ + allowEditorOverflow: false, + minContentWidthInPx: constObservable(0), + position: constObservable(null), + domNode: element.root, + })); }); constructor( private readonly _editor: ICodeEditor, ) { super(); - - this._register(this._editorObs.setDecorations(this._decorations)); - } - - public setPlaceholderText(placeholder: string): void { - this._placeholderText.set(placeholder, undefined); + this._view.recomputeInitiallyAndOnChange(this._store); } } -registerEditorContribution(PlaceholderTextContribution.ID, PlaceholderTextContribution, EditorContributionInstantiation.Lazy); +function isOrWasTrue(owner: DebugOwner, fn: (reader: IReader) => boolean): IObservable { + return derivedObservableWithCache(owner, (reader, lastValue) => { + if (lastValue === true) { return true; } + return fn(reader); + }); +} diff --git a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts index de4198e7446..05c099c63c4 100644 --- a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts @@ -12,7 +12,7 @@ import { IRange } from 'vs/editor/common/core/range'; import { IDiffEditor, IEditor, ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, ITextModel, OverviewRulerLane } from 'vs/editor/common/model'; import { overviewRulerRangeHighlight } from 'vs/editor/common/core/editorColorRegistry'; -import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickAccessProvider, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { IKeyMods, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { status } from 'vs/base/browser/ui/aria/aria'; @@ -52,7 +52,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu //#region Provider methods - provide(picker: IQuickPick, token: CancellationToken): IDisposable { + provide(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const disposables = new DisposableStore(); // Apply options if any @@ -63,7 +63,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu // Provide based on current active editor const pickerDisposable = disposables.add(new MutableDisposable()); - pickerDisposable.value = this.doProvide(picker, token); + pickerDisposable.value = this.doProvide(picker, token, runOptions); // Re-create whenever the active editor changes disposables.add(this.onDidActiveTextEditorControlChange(() => { @@ -78,7 +78,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu return disposables; } - private doProvide(picker: IQuickPick, token: CancellationToken): IDisposable { + private doProvide(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const disposables = new DisposableStore(); // With text control @@ -113,7 +113,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu disposables.add(toDisposable(() => this.clearDecorations(editor))); // Ask subclass for entries - disposables.add(this.provideWithTextEditor(context, picker, token)); + disposables.add(this.provideWithTextEditor(context, picker, token, runOptions)); } // Without text control @@ -134,7 +134,7 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu /** * Subclasses to implement to provide picks for the picker when an editor is active. */ - protected abstract provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable; + protected abstract provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable; /** * Subclasses to implement to provide picks for the picker when no editor is active. diff --git a/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts index 01092241b7a..d34aeb3faf5 100644 --- a/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts @@ -22,23 +22,33 @@ import { IQuickInputButton, IQuickPick, IQuickPickItem, IQuickPickSeparator } fr import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { Position } from 'vs/editor/common/core/position'; import { findLast } from 'vs/base/common/arraysFind'; +import { IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; +import { URI } from 'vs/base/common/uri'; export interface IGotoSymbolQuickPickItem extends IQuickPickItem { kind: SymbolKind; index: number; score?: number; + uri?: URI; + symbolName?: string; range?: { decoration: IRange; selection: IRange }; } export interface IGotoSymbolQuickAccessProviderOptions extends IEditorNavigationQuickAccessOptions { openSideBySideDirection?: () => undefined | 'right' | 'down'; + /** + * A handler to invoke when an item is accepted for + * this particular showing of the quick access. + * @param item The item that was accepted. + */ + readonly handleAccept?: (item: IQuickPickItem) => void; } export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider { static PREFIX = '@'; static SCOPE_PREFIX = ':'; - static PREFIX_BY_CATEGORY = `${AbstractGotoSymbolQuickAccessProvider.PREFIX}${AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX}`; + static PREFIX_BY_CATEGORY = `${this.PREFIX}${this.SCOPE_PREFIX}`; protected override readonly options: IGotoSymbolQuickAccessProviderOptions; @@ -59,7 +69,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return Disposable.None; } - protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable { + protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const editor = context.editor; const model = this.getModel(editor); if (!model) { @@ -68,7 +78,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit // Provide symbols from model if available in registry if (this._languageFeaturesService.documentSymbolProvider.has(model)) { - return this.doProvideWithEditorSymbols(context, model, picker, token); + return this.doProvideWithEditorSymbols(context, model, picker, token, runOptions); } // Otherwise show an entry for a model without registry @@ -127,7 +137,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return symbolProviderRegistryPromise.p; } - private doProvideWithEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick, token: CancellationToken): IDisposable { + private doProvideWithEditorSymbols(context: IQuickAccessTextEditorContext, model: ITextModel, picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { const editor = context.editor; const disposables = new DisposableStore(); @@ -137,6 +147,8 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit if (item && item.range) { this.gotoLocation(context, { range: item.range.selection, keyMods: picker.keyMods, preserveFocus: event.inBackground }); + runOptions?.handleAccept?.(item); + if (!event.inBackground) { picker.hide(); } @@ -171,7 +183,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit picker.busy = true; try { const query = prepareQuery(picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim()); - const items = await this.doGetSymbolPicks(symbolsPromise, query, undefined, picksCts.token); + const items = await this.doGetSymbolPicks(symbolsPromise, query, undefined, picksCts.token, model); if (token.isCancellationRequested) { return; } @@ -218,7 +230,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return disposables; } - protected async doGetSymbolPicks(symbolsPromise: Promise, query: IPreparedQuery, options: { extraContainerLabel?: string } | undefined, token: CancellationToken): Promise> { + protected async doGetSymbolPicks(symbolsPromise: Promise, query: IPreparedQuery, options: { extraContainerLabel?: string } | undefined, token: CancellationToken, model: ITextModel): Promise> { const symbols = await symbolsPromise; if (token.isCancellationRequested) { return []; @@ -326,6 +338,8 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit selection: Range.collapseToStart(symbol.selectionRange), decoration: symbol.range }, + uri: model.uri, + symbolName: symbolLabel, strikethrough: deprecated, buttons }); diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.ts b/src/vs/editor/contrib/rename/browser/renameWidget.ts index 03257f28aa3..b284cd519fd 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.ts +++ b/src/vs/editor/contrib/rename/browser/renameWidget.ts @@ -6,7 +6,7 @@ import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; @@ -32,7 +32,6 @@ import { IRange, Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { NewSymbolName, NewSymbolNameTag, NewSymbolNameTriggerKind, ProviderResult } from 'vs/editor/common/languages'; import * as nls from 'vs/nls'; -import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILogService } from 'vs/platform/log/common/log'; @@ -55,8 +54,8 @@ const _sticky = false ; -export const CONTEXT_RENAME_INPUT_VISIBLE = new RawContextKey('renameInputVisible', false, localize('renameInputVisible', "Whether the rename input widget is visible")); -export const CONTEXT_RENAME_INPUT_FOCUSED = new RawContextKey('renameInputFocused', false, localize('renameInputFocused', "Whether the rename input widget is focused")); +export const CONTEXT_RENAME_INPUT_VISIBLE = new RawContextKey('renameInputVisible', false, nls.localize('renameInputVisible', "Whether the rename input widget is visible")); +export const CONTEXT_RENAME_INPUT_FOCUSED = new RawContextKey('renameInputFocused', false, nls.localize('renameInputFocused', "Whether the rename input widget is focused")); /** * "Source" of the new name: @@ -311,7 +310,7 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable beforeRender(): IDimension | null { const [accept, preview] = this._acceptKeybindings; - this._label!.innerText = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Rename, Shift+F2 to Preview"'] }, "{0} to Rename, {1} to Preview", this._keybindingService.lookupKeybinding(accept)?.getLabel(), this._keybindingService.lookupKeybinding(preview)?.getLabel()); + this._label!.innerText = nls.localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Rename, Shift+F2 to Preview"'] }, "{0} to Rename, {1} to Preview", this._keybindingService.lookupKeybinding(accept)?.getLabel(), this._keybindingService.lookupKeybinding(preview)?.getLabel()); this._domNode!.style.minWidth = `200px`; // to prevent from widening when candidates come in @@ -750,7 +749,7 @@ class RenameCandidateListView { this._listContainer.style.height = `${height}px`; this._listContainer.style.width = `${width}px`; - aria.status(localize('renameSuggestionsReceivedAria', "Received {0} rename suggestions", candidates.length)); + aria.status(nls.localize('renameSuggestionsReceivedAria', "Received {0} rename suggestions", candidates.length)); } public clearCandidates(): void { @@ -900,7 +899,7 @@ class InputWithButton implements IDisposable { private _domNode: HTMLDivElement | undefined; private _inputNode: HTMLInputElement | undefined; private _buttonNode: HTMLElement | undefined; - private _buttonHover: IUpdatableHover | undefined; + private _buttonHover: IManagedHover | undefined; private _buttonGenHoverText: string | undefined; private _buttonCancelHoverText: string | undefined; private _sparkleIcon: HTMLElement | undefined; @@ -924,7 +923,7 @@ class InputWithButton implements IDisposable { this._inputNode.className = 'rename-input'; this._inputNode.type = 'text'; this._inputNode.style.border = 'none'; - this._inputNode.setAttribute('aria-label', localize('renameAriaLabel', "Rename input. Type new name and press Enter to commit.")); + this._inputNode.setAttribute('aria-label', nls.localize('renameAriaLabel', "Rename input. Type new name and press Enter to commit.")); this._domNode.appendChild(this._inputNode); @@ -934,7 +933,7 @@ class InputWithButton implements IDisposable { this._buttonGenHoverText = nls.localize('generateRenameSuggestionsButton', "Generate new name suggestions"); this._buttonCancelHoverText = nls.localize('cancelRenameSuggestionsButton', "Cancel"); - this._buttonHover = getBaseLayerHoverDelegate().setupUpdatableHover(getDefaultHoverDelegate('element'), this._buttonNode, this._buttonGenHoverText); + this._buttonHover = getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('element'), this._buttonNode, this._buttonGenHoverText); this._disposables.add(this._buttonHover); this._domNode.appendChild(this._buttonNode); diff --git a/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts index c2d9a57c202..86d3bf5d62d 100644 --- a/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts +++ b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Barrier, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts b/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts index da8dc2f222d..6ac97ecde96 100644 --- a/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts +++ b/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts index dc9e9767d81..1de9179cc28 100644 --- a/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts index e0f11350e89..236ad70f27d 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts index 1c9c3a80cdb..5b9a5e434f8 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts index ee2f1f1d24f..bbf24168409 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Choice, FormatString, Marker, Placeholder, Scanner, SnippetParser, Text, TextmateSnippet, TokenType, Transform, Variable } from 'vs/editor/contrib/snippet/browser/snippetParser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts index 61061846724..0a9a3c76462 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts index 53a9b272757..b8ea4e52342 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { sep } from 'vs/base/common/path'; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index 3bc52c6c915..276082256f6 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -61,7 +61,7 @@ .monaco-editor .sticky-widget { width: 100%; - box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; + box-shadow: var(--vscode-editorStickyScroll-shadow) 0 4px 2px -2px; z-index: 4; background-color: var(--vscode-editorStickyScroll-background); right: initial !important; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index bdc0542b18e..22b8c67a294 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -51,7 +51,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib private readonly _sessionStore: DisposableStore = new DisposableStore(); private _widgetState: StickyScrollWidgetState; - private _foldingModel: FoldingModel | null = null; + private _foldingModel: FoldingModel | undefined; private _maxStickyLines: number = Number.MAX_SAFE_INTEGER; private _stickyRangeProjectedOnEditor: IRange | undefined; @@ -67,7 +67,8 @@ export class StickyScrollController extends Disposable implements IEditorContrib private _positionRevealed = false; private _onMouseDown = false; private _endLineNumbers: number[] = []; - private _showEndForLine: number | null = null; + private _showEndForLine: number | undefined; + private _minRebuildFromLine: number | undefined; constructor( private readonly _editor: ICodeEditor, @@ -84,7 +85,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._register(this._stickyScrollWidget); this._register(this._stickyLineCandidateProvider); - this._widgetState = new StickyScrollWidgetState([], [], 0); + this._widgetState = StickyScrollWidgetState.Empty; this._onDidResize(); this._readConfiguration(); const stickyScrollDomNode = this._stickyScrollWidget.getDomNode(); @@ -291,14 +292,14 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._renderStickyScroll(); return; } - if (this._showEndForLine !== null) { - this._showEndForLine = null; + if (this._showEndForLine !== undefined) { + this._showEndForLine = undefined; this._renderStickyScroll(); } })); this._register(dom.addDisposableListener(stickyScrollWidgetDomNode, dom.EventType.MOUSE_LEAVE, (e) => { - if (this._showEndForLine !== null) { - this._showEndForLine = null; + if (this._showEndForLine !== undefined) { + this._showEndForLine = undefined; this._renderStickyScroll(); } })); @@ -415,14 +416,14 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._editor.addOverlayWidget(this._stickyScrollWidget); this._sessionStore.add(this._editor.onDidScrollChange((e) => { if (e.scrollTopChanged) { - this._showEndForLine = null; + this._showEndForLine = undefined; this._renderStickyScroll(); } })); this._sessionStore.add(this._editor.onDidLayoutChange(() => this._onDidResize())); this._sessionStore.add(this._editor.onDidChangeModelTokens((e) => this._onTokensChange(e))); this._sessionStore.add(this._stickyLineCandidateProvider.onDidChangeStickyScroll(() => { - this._showEndForLine = null; + this._showEndForLine = undefined; this._renderStickyScroll(); })); this._enabled = true; @@ -431,7 +432,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib const lineNumberOption = this._editor.getOption(EditorOption.lineNumbers); if (lineNumberOption.renderType === RenderLineNumbersType.Relative) { this._sessionStore.add(this._editor.onDidChangeCursorPosition(() => { - this._showEndForLine = null; + this._showEndForLine = undefined; this._renderStickyScroll(0); })); } @@ -479,32 +480,29 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._maxStickyLines = Math.round(theoreticalLines * .25); } - private async _renderStickyScroll(rebuildFromLine?: number) { + private async _renderStickyScroll(rebuildFromLine?: number): Promise { const model = this._editor.getModel(); if (!model || model.isTooLargeForTokenization()) { - this._foldingModel = null; - this._stickyScrollWidget.setState(undefined, null); + this._resetState(); return; } - const stickyLineVersion = this._stickyLineCandidateProvider.getVersionId(); - if (stickyLineVersion === undefined || stickyLineVersion === model.getVersionId()) { - this._foldingModel = await FoldingController.get(this._editor)?.getFoldingModel() ?? null; - this._widgetState = this.findScrollWidgetState(); - this._stickyScrollVisibleContextKey.set(!(this._widgetState.startLineNumbers.length === 0)); - + const nextRebuildFromLine = this._updateAndGetMinRebuildFromLine(rebuildFromLine); + const stickyWidgetVersion = this._stickyLineCandidateProvider.getVersionId(); + const shouldUpdateState = stickyWidgetVersion === undefined || stickyWidgetVersion === model.getVersionId(); + if (shouldUpdateState) { if (!this._focused) { - this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + await this._updateState(nextRebuildFromLine); } else { // Suppose that previously the sticky scroll widget had height 0, then if there are visible lines, set the last line as focused if (this._focusedStickyElementIndex === -1) { - this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + await this._updateState(nextRebuildFromLine); this._focusedStickyElementIndex = this._stickyScrollWidget.lineNumberCount - 1; if (this._focusedStickyElementIndex !== -1) { this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex); } } else { const focusedStickyElementLineNumber = this._stickyScrollWidget.lineNumbers[this._focusedStickyElementIndex]; - this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + await this._updateState(nextRebuildFromLine); // Suppose that after setting the state, there are no sticky lines, set the focused index to -1 if (this._stickyScrollWidget.lineNumberCount === 0) { this._focusedStickyElementIndex = -1; @@ -523,6 +521,31 @@ export class StickyScrollController extends Disposable implements IEditorContrib } } + private _updateAndGetMinRebuildFromLine(rebuildFromLine: number | undefined): number | undefined { + if (rebuildFromLine !== undefined) { + const minRebuildFromLineOrInfinity = this._minRebuildFromLine !== undefined ? this._minRebuildFromLine : Infinity; + this._minRebuildFromLine = Math.min(rebuildFromLine, minRebuildFromLineOrInfinity); + } + return this._minRebuildFromLine; + } + + private async _updateState(rebuildFromLine?: number): Promise { + this._minRebuildFromLine = undefined; + this._foldingModel = await FoldingController.get(this._editor)?.getFoldingModel() ?? undefined; + this._widgetState = this.findScrollWidgetState(); + const stickyWidgetHasLines = this._widgetState.startLineNumbers.length > 0; + this._stickyScrollVisibleContextKey.set(stickyWidgetHasLines); + this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); + } + + private async _resetState(): Promise { + this._minRebuildFromLine = undefined; + this._foldingModel = undefined; + this._widgetState = StickyScrollWidgetState.Empty; + this._stickyScrollVisibleContextKey.set(false); + this._stickyScrollWidget.setState(undefined, undefined); + } + findScrollWidgetState(): StickyScrollWidgetState { const lineHeight: number = this._editor.getOption(EditorOption.lineHeight); const maxNumberStickyLines = Math.min(this._maxStickyLines, this._editor.getOption(EditorOption.stickyScroll).maxLineCount); diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index b6a46b82ebd..5e8f0ff94ff 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -36,6 +36,10 @@ export class StickyScrollWidgetState { && equals(this.startLineNumbers, other.startLineNumbers) && equals(this.endLineNumbers, other.endLineNumbers); } + + static get Empty() { + return new StickyScrollWidgetState([], [], 0); + } } const _ttPolicy = createTrustedTypesPolicy('stickyScrollViewLayer', { createHTML: value => value }); @@ -129,7 +133,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { return this._lineNumbers; } - setState(_state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | null, _rebuildFromLine?: number): void { + setState(_state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | undefined, _rebuildFromLine?: number): void { if (_rebuildFromLine === undefined && ((!this._previousState && !_state) || (this._previousState && this._previousState.equals(_state))) ) { @@ -211,7 +215,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } } - private async _renderRootNode(state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | null, rebuildFromLine: number): Promise { + private async _renderRootNode(state: StickyScrollWidgetState | undefined, foldingModel: FoldingModel | undefined, rebuildFromLine: number): Promise { this._clearStickyLinesFromLine(rebuildFromLine); if (!state) { return; @@ -264,7 +268,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { })); } - private _renderChildNode(index: number, line: number, foldingModel: FoldingModel | null, layoutInfo: EditorLayoutInfo): RenderedStickyLine | undefined { + private _renderChildNode(index: number, line: number, foldingModel: FoldingModel | undefined, layoutInfo: EditorLayoutInfo): RenderedStickyLine | undefined { const viewModel = this._editor._getViewModel(); if (!viewModel) { return; @@ -364,7 +368,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { return stickyLine; } - private _renderFoldingIconForLine(foldingModel: FoldingModel | null, line: number): StickyFoldingIcon | undefined { + private _renderFoldingIconForLine(foldingModel: FoldingModel | undefined, line: number): StickyFoldingIcon | undefined { const showFoldingControls: 'mouseover' | 'always' | 'never' = this._editor.getOption(EditorOption.showFoldingControls); if (!foldingModel || showFoldingControls === 'never') { return; diff --git a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts index 04e852765ca..aaefcb65f3d 100644 --- a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts +++ b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { StickyScrollController } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollController'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 2eeb94d99b6..2d30e5925ab 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -39,15 +39,15 @@ import { status } from 'vs/base/browser/ui/aria/aria'; /** * Suggest widget colors */ -registerColor('editorSuggestWidget.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.')); -registerColor('editorSuggestWidget.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.')); -const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', { dark: editorForeground, light: editorForeground, hcDark: editorForeground, hcLight: editorForeground }, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.')); -registerColor('editorSuggestWidget.selectedForeground', { dark: quickInputListFocusForeground, light: quickInputListFocusForeground, hcDark: quickInputListFocusForeground, hcLight: quickInputListFocusForeground }, nls.localize('editorSuggestWidgetSelectedForeground', 'Foreground color of the selected entry in the suggest widget.')); -registerColor('editorSuggestWidget.selectedIconForeground', { dark: quickInputListFocusIconForeground, light: quickInputListFocusIconForeground, hcDark: quickInputListFocusIconForeground, hcLight: quickInputListFocusIconForeground }, nls.localize('editorSuggestWidgetSelectedIconForeground', 'Icon foreground color of the selected entry in the suggest widget.')); -export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: quickInputListFocusBackground, light: quickInputListFocusBackground, hcDark: quickInputListFocusBackground, hcLight: quickInputListFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); -registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hcDark: listHighlightForeground, hcLight: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); -registerColor('editorSuggestWidget.focusHighlightForeground', { dark: listFocusHighlightForeground, light: listFocusHighlightForeground, hcDark: listFocusHighlightForeground, hcLight: listFocusHighlightForeground }, nls.localize('editorSuggestWidgetFocusHighlightForeground', 'Color of the match highlights in the suggest widget when an item is focused.')); -registerColor('editorSuggestWidgetStatus.foreground', { dark: transparent(editorSuggestWidgetForeground, .5), light: transparent(editorSuggestWidgetForeground, .5), hcDark: transparent(editorSuggestWidgetForeground, .5), hcLight: transparent(editorSuggestWidgetForeground, .5) }, nls.localize('editorSuggestWidgetStatusForeground', 'Foreground color of the suggest widget status.')); +registerColor('editorSuggestWidget.background', editorWidgetBackground, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.')); +registerColor('editorSuggestWidget.border', editorWidgetBorder, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.')); +const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', editorForeground, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.')); +registerColor('editorSuggestWidget.selectedForeground', quickInputListFocusForeground, nls.localize('editorSuggestWidgetSelectedForeground', 'Foreground color of the selected entry in the suggest widget.')); +registerColor('editorSuggestWidget.selectedIconForeground', quickInputListFocusIconForeground, nls.localize('editorSuggestWidgetSelectedIconForeground', 'Icon foreground color of the selected entry in the suggest widget.')); +export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', quickInputListFocusBackground, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); +registerColor('editorSuggestWidget.highlightForeground', listHighlightForeground, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); +registerColor('editorSuggestWidget.focusHighlightForeground', listFocusHighlightForeground, nls.localize('editorSuggestWidgetFocusHighlightForeground', 'Color of the match highlights in the suggest widget when an item is focused.')); +registerColor('editorSuggestWidgetStatus.foreground', transparent(editorSuggestWidgetForeground, .5), nls.localize('editorSuggestWidgetStatusForeground', 'Foreground color of the suggest widget status.')); const enum State { Hidden, diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts b/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts index 25d19c054d2..4a1df5b9ce8 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts @@ -6,31 +6,12 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction } from 'vs/base/common/actions'; -import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { localize } from 'vs/nls'; -import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { TextOnlyMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -class StatusBarViewItem extends MenuEntryActionViewItem { - - protected override updateLabel() { - const kb = this._keybindingService.lookupKeybinding(this._action.id, this._contextKeyService); - if (!kb) { - return super.updateLabel(); - } - if (this.label) { - this.label.textContent = localize({ key: 'content', comment: ['A label', 'A keybinding'] }, '{0} ({1})', this._action.label, StatusBarViewItem.symbolPrintEnter(kb)); - } - } - - static symbolPrintEnter(kb: ResolvedKeybinding) { - return kb.getLabel()?.replace(/\benter\b/gi, '\u23CE'); - } -} - export class SuggestWidgetStatus { readonly element: HTMLElement; @@ -49,7 +30,7 @@ export class SuggestWidgetStatus { this.element = dom.append(container, dom.$('.suggest-status-bar')); const actionViewItemProvider = (action => { - return action instanceof MenuItemAction ? instantiationService.createInstance(StatusBarViewItem, action, undefined) : undefined; + return action instanceof MenuItemAction ? instantiationService.createInstance(TextOnlyMenuEntryActionViewItem, action, { useComma: true }) : undefined; }); this._leftActions = new ActionBar(this.element, { actionViewItemProvider }); this._rightActions = new ActionBar(this.element, { actionViewItemProvider }); diff --git a/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts b/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts index cb08e56fb7f..823d6865a84 100644 --- a/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/completionModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorOptions, InternalSuggestOptions } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts index d4df28ea332..b2e9fc03577 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggest.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts index 617019ff5cf..64123de47db 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestController.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts index 1aaca84d484..91d42374b1f 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts index 3bbc8c58838..57d0d030d48 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestMemory.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IPosition } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts index 2bbfe524bfb..5c673c0f355 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts b/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts index 04f7bce1cc1..15f238b1627 100644 --- a/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts index 64ccb1bc618..7d6fe4b73f6 100644 --- a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts +++ b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts @@ -7,19 +7,9 @@ import 'vs/css!./symbolIcons'; import { localize } from 'vs/nls'; import { foreground, registerColor } from 'vs/platform/theme/common/colorRegistry'; -export const SYMBOL_ICON_ARRAY_FOREGROUND = registerColor('symbolIcon.arrayForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground, -}, localize('symbolIcon.arrayForeground', 'The foreground color for array symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_ARRAY_FOREGROUND = registerColor('symbolIcon.arrayForeground', foreground, localize('symbolIcon.arrayForeground', 'The foreground color for array symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_BOOLEAN_FOREGROUND = registerColor('symbolIcon.booleanForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground, -}, localize('symbolIcon.booleanForeground', 'The foreground color for boolean symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_BOOLEAN_FOREGROUND = registerColor('symbolIcon.booleanForeground', foreground, localize('symbolIcon.booleanForeground', 'The foreground color for boolean symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_CLASS_FOREGROUND = registerColor('symbolIcon.classForeground', { dark: '#EE9D28', @@ -28,19 +18,9 @@ export const SYMBOL_ICON_CLASS_FOREGROUND = registerColor('symbolIcon.classForeg hcLight: '#D67E00' }, localize('symbolIcon.classForeground', 'The foreground color for class symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_COLOR_FOREGROUND = registerColor('symbolIcon.colorForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.colorForeground', 'The foreground color for color symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_COLOR_FOREGROUND = registerColor('symbolIcon.colorForeground', foreground, localize('symbolIcon.colorForeground', 'The foreground color for color symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_CONSTANT_FOREGROUND = registerColor('symbolIcon.constantForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.constantForeground', 'The foreground color for constant symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_CONSTANT_FOREGROUND = registerColor('symbolIcon.constantForeground', foreground, localize('symbolIcon.constantForeground', 'The foreground color for constant symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_CONSTRUCTOR_FOREGROUND = registerColor('symbolIcon.constructorForeground', { dark: '#B180D7', @@ -77,19 +57,9 @@ export const SYMBOL_ICON_FIELD_FOREGROUND = registerColor('symbolIcon.fieldForeg hcLight: '#007ACC' }, localize('symbolIcon.fieldForeground', 'The foreground color for field symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_FILE_FOREGROUND = registerColor('symbolIcon.fileForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.fileForeground', 'The foreground color for file symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_FILE_FOREGROUND = registerColor('symbolIcon.fileForeground', foreground, localize('symbolIcon.fileForeground', 'The foreground color for file symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_FOLDER_FOREGROUND = registerColor('symbolIcon.folderForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.folderForeground', 'The foreground color for folder symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_FOLDER_FOREGROUND = registerColor('symbolIcon.folderForeground', foreground, localize('symbolIcon.folderForeground', 'The foreground color for folder symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_FUNCTION_FOREGROUND = registerColor('symbolIcon.functionForeground', { dark: '#B180D7', @@ -105,19 +75,9 @@ export const SYMBOL_ICON_INTERFACE_FOREGROUND = registerColor('symbolIcon.interf hcLight: '#007ACC' }, localize('symbolIcon.interfaceForeground', 'The foreground color for interface symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_KEY_FOREGROUND = registerColor('symbolIcon.keyForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.keyForeground', 'The foreground color for key symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_KEY_FOREGROUND = registerColor('symbolIcon.keyForeground', foreground, localize('symbolIcon.keyForeground', 'The foreground color for key symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_KEYWORD_FOREGROUND = registerColor('symbolIcon.keywordForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.keywordForeground', 'The foreground color for keyword symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_KEYWORD_FOREGROUND = registerColor('symbolIcon.keywordForeground', foreground, localize('symbolIcon.keywordForeground', 'The foreground color for keyword symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_METHOD_FOREGROUND = registerColor('symbolIcon.methodForeground', { dark: '#B180D7', @@ -126,110 +86,35 @@ export const SYMBOL_ICON_METHOD_FOREGROUND = registerColor('symbolIcon.methodFor hcLight: '#652D90' }, localize('symbolIcon.methodForeground', 'The foreground color for method symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_MODULE_FOREGROUND = registerColor('symbolIcon.moduleForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.moduleForeground', 'The foreground color for module symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_MODULE_FOREGROUND = registerColor('symbolIcon.moduleForeground', foreground, localize('symbolIcon.moduleForeground', 'The foreground color for module symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_NAMESPACE_FOREGROUND = registerColor('symbolIcon.namespaceForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.namespaceForeground', 'The foreground color for namespace symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_NAMESPACE_FOREGROUND = registerColor('symbolIcon.namespaceForeground', foreground, localize('symbolIcon.namespaceForeground', 'The foreground color for namespace symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_NULL_FOREGROUND = registerColor('symbolIcon.nullForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.nullForeground', 'The foreground color for null symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_NULL_FOREGROUND = registerColor('symbolIcon.nullForeground', foreground, localize('symbolIcon.nullForeground', 'The foreground color for null symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_NUMBER_FOREGROUND = registerColor('symbolIcon.numberForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.numberForeground', 'The foreground color for number symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_NUMBER_FOREGROUND = registerColor('symbolIcon.numberForeground', foreground, localize('symbolIcon.numberForeground', 'The foreground color for number symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_OBJECT_FOREGROUND = registerColor('symbolIcon.objectForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.objectForeground', 'The foreground color for object symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_OBJECT_FOREGROUND = registerColor('symbolIcon.objectForeground', foreground, localize('symbolIcon.objectForeground', 'The foreground color for object symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_OPERATOR_FOREGROUND = registerColor('symbolIcon.operatorForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.operatorForeground', 'The foreground color for operator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_OPERATOR_FOREGROUND = registerColor('symbolIcon.operatorForeground', foreground, localize('symbolIcon.operatorForeground', 'The foreground color for operator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_PACKAGE_FOREGROUND = registerColor('symbolIcon.packageForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.packageForeground', 'The foreground color for package symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_PACKAGE_FOREGROUND = registerColor('symbolIcon.packageForeground', foreground, localize('symbolIcon.packageForeground', 'The foreground color for package symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_PROPERTY_FOREGROUND = registerColor('symbolIcon.propertyForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.propertyForeground', 'The foreground color for property symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_PROPERTY_FOREGROUND = registerColor('symbolIcon.propertyForeground', foreground, localize('symbolIcon.propertyForeground', 'The foreground color for property symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_REFERENCE_FOREGROUND = registerColor('symbolIcon.referenceForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.referenceForeground', 'The foreground color for reference symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_REFERENCE_FOREGROUND = registerColor('symbolIcon.referenceForeground', foreground, localize('symbolIcon.referenceForeground', 'The foreground color for reference symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_SNIPPET_FOREGROUND = registerColor('symbolIcon.snippetForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.snippetForeground', 'The foreground color for snippet symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_SNIPPET_FOREGROUND = registerColor('symbolIcon.snippetForeground', foreground, localize('symbolIcon.snippetForeground', 'The foreground color for snippet symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_STRING_FOREGROUND = registerColor('symbolIcon.stringForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.stringForeground', 'The foreground color for string symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_STRING_FOREGROUND = registerColor('symbolIcon.stringForeground', foreground, localize('symbolIcon.stringForeground', 'The foreground color for string symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_STRUCT_FOREGROUND = registerColor('symbolIcon.structForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground, -}, localize('symbolIcon.structForeground', 'The foreground color for struct symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_STRUCT_FOREGROUND = registerColor('symbolIcon.structForeground', foreground, localize('symbolIcon.structForeground', 'The foreground color for struct symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_TEXT_FOREGROUND = registerColor('symbolIcon.textForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.textForeground', 'The foreground color for text symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_TEXT_FOREGROUND = registerColor('symbolIcon.textForeground', foreground, localize('symbolIcon.textForeground', 'The foreground color for text symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_TYPEPARAMETER_FOREGROUND = registerColor('symbolIcon.typeParameterForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.typeParameterForeground', 'The foreground color for type parameter symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_TYPEPARAMETER_FOREGROUND = registerColor('symbolIcon.typeParameterForeground', foreground, localize('symbolIcon.typeParameterForeground', 'The foreground color for type parameter symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_UNIT_FOREGROUND = registerColor('symbolIcon.unitForeground', { - dark: foreground, - light: foreground, - hcDark: foreground, - hcLight: foreground -}, localize('symbolIcon.unitForeground', 'The foreground color for unit symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); +export const SYMBOL_ICON_UNIT_FOREGROUND = registerColor('symbolIcon.unitForeground', foreground, localize('symbolIcon.unitForeground', 'The foreground color for unit symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_VARIABLE_FOREGROUND = registerColor('symbolIcon.variableForeground', { dark: '#75BEFF', diff --git a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts index f44fc76e0bd..eae47ac2d71 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts @@ -7,7 +7,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { CharCode } from 'vs/base/common/charCode'; import { Codicon } from 'vs/base/common/codicons'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { InvisibleCharacters, isBasicASCII } from 'vs/base/common/strings'; import 'vs/css!./unicodeHighlighter'; @@ -22,7 +22,7 @@ import { UnicodeHighlighterOptions, UnicodeHighlighterReason, UnicodeHighlighter import { IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { isModelDecorationInComment, isModelDecorationInString, isModelDecorationVisible } from 'vs/editor/common/viewModel/viewModelDecorations'; -import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { MarkdownHover, renderMarkdownHovers } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; import { BannerController } from 'vs/editor/contrib/unicodeHighlighter/browser/bannerController'; import * as nls from 'vs/nls'; @@ -506,9 +506,13 @@ export class UnicodeHighlighterHoverParticipant implements IEditorHoverParticipa return result; } - public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: MarkdownHover[]): IDisposable { + public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: MarkdownHover[]): IRenderedHoverParts { return renderMarkdownHovers(context, hoverParts, this._editor, this._languageService, this._openerService); } + + public getAccessibleContent(hoverPart: MarkdownHover): string { + return hoverPart.contents.map(c => c.value).join('\n'); + } } function codePointToHex(codePoint: number): string { diff --git a/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts b/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts index b3825b0f05d..79ec7ad02e6 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/highlightDecorations.ts @@ -13,13 +13,13 @@ import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/ const wordHighlightBackground = registerColor('editor.wordHighlightBackground', { dark: '#575757B8', light: '#57575740', hcDark: null, hcLight: null }, nls.localize('wordHighlight', 'Background color of a symbol during read-access, like reading a variable. The color must not be opaque so as not to hide underlying decorations.'), true); registerColor('editor.wordHighlightStrongBackground', { dark: '#004972B8', light: '#0e639c40', hcDark: null, hcLight: null }, nls.localize('wordHighlightStrong', 'Background color of a symbol during write-access, like writing to a variable. The color must not be opaque so as not to hide underlying decorations.'), true); -registerColor('editor.wordHighlightTextBackground', { light: wordHighlightBackground, dark: wordHighlightBackground, hcDark: wordHighlightBackground, hcLight: wordHighlightBackground }, nls.localize('wordHighlightText', 'Background color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); +registerColor('editor.wordHighlightTextBackground', wordHighlightBackground, nls.localize('wordHighlightText', 'Background color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); const wordHighlightBorder = registerColor('editor.wordHighlightBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('wordHighlightBorder', 'Border color of a symbol during read-access, like reading a variable.')); registerColor('editor.wordHighlightStrongBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('wordHighlightStrongBorder', 'Border color of a symbol during write-access, like writing to a variable.')); -registerColor('editor.wordHighlightTextBorder', { light: wordHighlightBorder, dark: wordHighlightBorder, hcDark: wordHighlightBorder, hcLight: wordHighlightBorder }, nls.localize('wordHighlightTextBorder', "Border color of a textual occurrence for a symbol.")); -const overviewRulerWordHighlightForeground = registerColor('editorOverviewRuler.wordHighlightForeground', { dark: '#A0A0A0CC', light: '#A0A0A0CC', hcDark: '#A0A0A0CC', hcLight: '#A0A0A0CC' }, nls.localize('overviewRulerWordHighlightForeground', 'Overview ruler marker color for symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); -const overviewRulerWordHighlightStrongForeground = registerColor('editorOverviewRuler.wordHighlightStrongForeground', { dark: '#C0A0C0CC', light: '#C0A0C0CC', hcDark: '#C0A0C0CC', hcLight: '#C0A0C0CC' }, nls.localize('overviewRulerWordHighlightStrongForeground', 'Overview ruler marker color for write-access symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); -const overviewRulerWordHighlightTextForeground = registerColor('editorOverviewRuler.wordHighlightTextForeground', { dark: overviewRulerSelectionHighlightForeground, light: overviewRulerSelectionHighlightForeground, hcDark: overviewRulerSelectionHighlightForeground, hcLight: overviewRulerSelectionHighlightForeground }, nls.localize('overviewRulerWordHighlightTextForeground', 'Overview ruler marker color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); +registerColor('editor.wordHighlightTextBorder', wordHighlightBorder, nls.localize('wordHighlightTextBorder', "Border color of a textual occurrence for a symbol.")); +const overviewRulerWordHighlightForeground = registerColor('editorOverviewRuler.wordHighlightForeground', '#A0A0A0CC', nls.localize('overviewRulerWordHighlightForeground', 'Overview ruler marker color for symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); +const overviewRulerWordHighlightStrongForeground = registerColor('editorOverviewRuler.wordHighlightStrongForeground', '#C0A0C0CC', nls.localize('overviewRulerWordHighlightStrongForeground', 'Overview ruler marker color for write-access symbol highlights. The color must not be opaque so as not to hide underlying decorations.'), true); +const overviewRulerWordHighlightTextForeground = registerColor('editorOverviewRuler.wordHighlightTextForeground', overviewRulerSelectionHighlightForeground, nls.localize('overviewRulerWordHighlightTextForeground', 'Overview ruler marker color of a textual occurrence for a symbol. The color must not be opaque so as not to hide underlying decorations.'), true); const _WRITE_OPTIONS = ModelDecorationOptions.register({ description: 'word-highlight-strong', diff --git a/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts b/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts index 388d42021d2..29381ecf63f 100644 --- a/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts @@ -48,10 +48,10 @@ export abstract class MoveWordCommand extends EditorCommand { const wordSeparators = getMapForWordSeparators(editor.getOption(EditorOption.wordSeparators), editor.getOption(EditorOption.wordSegmenterLocales)); const model = editor.getModel(); const selections = editor.getSelections(); - + const hasMulticursor = selections.length > 1; const result = selections.map((sel) => { const inPosition = new Position(sel.positionLineNumber, sel.positionColumn); - const outPosition = this._move(wordSeparators, model, inPosition, this._wordNavigationType); + const outPosition = this._move(wordSeparators, model, inPosition, this._wordNavigationType, hasMulticursor); return this._moveTo(sel, outPosition, this._inSelectionMode); }); @@ -83,17 +83,17 @@ export abstract class MoveWordCommand extends EditorCommand { } } - protected abstract _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position; + protected abstract _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position; } export class WordLeftCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return WordOperations.moveWordLeft(wordSeparators, model, position, wordNavigationType); + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return WordOperations.moveWordLeft(wordSeparators, model, position, wordNavigationType, hasMulticursor); } } export class WordRightCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { return WordOperations.moveWordRight(wordSeparators, model, position, wordNavigationType); } } @@ -187,8 +187,8 @@ export class CursorWordAccessibilityLeft extends WordLeftCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } @@ -202,8 +202,8 @@ export class CursorWordAccessibilityLeftSelect extends WordLeftCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } @@ -295,8 +295,8 @@ export class CursorWordAccessibilityRight extends WordRightCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } @@ -310,8 +310,8 @@ export class CursorWordAccessibilityRightSelect extends WordRightCommand { }); } - protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType); + protected override _move(wordCharacterClassifier: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return super._move(getMapForWordSeparators(EditorOptions.wordSeparators.defaultValue, wordCharacterClassifier.intlSegmenterLocales), model, position, wordNavigationType, hasMulticursor); } } diff --git a/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts index a06bf07200a..d90d44fd2d6 100644 --- a/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { isFirefox } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -179,7 +180,11 @@ suite('WordOperations', () => { assert.deepStrictEqual(actual, EXPECTED); }); - test('cursorWordLeft - Recognize words', () => { + test('cursorWordLeft - Recognize words', function () { + if (isFirefox) { + // https://github.com/microsoft/vscode/issues/219843 + return this.skip(); + } const EXPECTED = [ '|/* |ã“れ|ã¯|テスト|ã§ã™ |/*', ].join('\n'); @@ -217,6 +222,40 @@ suite('WordOperations', () => { assert.deepStrictEqual(actual, EXPECTED); }); + test('cursorWordLeft - issue #169904: cursors out of sync', () => { + const text = [ + '.grid1 {', + ' display: grid;', + ' grid-template-columns:', + ' [full-start] minmax(1em, 1fr)', + ' [main-start] minmax(0, 40em) [main-end]', + ' minmax(1em, 1fr) [full-end];', + '}', + '.grid2 {', + ' display: grid;', + ' grid-template-columns:', + ' [full-start] minmax(1em, 1fr)', + ' [main-start] minmax(0, 40em) [main-end] minmax(1em, 1fr) [full-end];', + '}', + ]; + withTestCodeEditor(text, {}, (editor) => { + editor.setSelections([ + new Selection(5, 44, 5, 44), + new Selection(6, 32, 6, 32), + new Selection(12, 44, 12, 44), + new Selection(12, 72, 12, 72), + ]); + cursorWordLeft(editor, false); + assert.deepStrictEqual(editor.getSelections(), [ + new Selection(5, 43, 5, 43), + new Selection(6, 31, 6, 31), + new Selection(12, 43, 12, 43), + new Selection(12, 71, 12, 71), + ]); + + }); + }); + test('cursorWordLeftSelect - issue #74369: cursorWordLeft and cursorWordLeftSelect do not behave consistently', () => { const EXPECTED = [ '|this.|is.|a.|test', @@ -365,7 +404,11 @@ suite('WordOperations', () => { assert.deepStrictEqual(actual, EXPECTED); }); - test('cursorWordRight - Recognize words', () => { + test('cursorWordRight - Recognize words', function () { + if (isFirefox) { + // https://github.com/microsoft/vscode/issues/219843 + return this.skip(); + } const EXPECTED = [ '/*| ã“れ|ã¯|テスト|ã§ã™|/*|', ].join('\n'); diff --git a/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts b/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts index ebcabc415a4..5ed4d310941 100644 --- a/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts +++ b/src/vs/editor/contrib/wordPartOperations/browser/wordPartOperations.ts @@ -68,8 +68,8 @@ export class DeleteWordPartRight extends DeleteWordCommand { } export class WordPartLeftCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { - return WordPartOperations.moveWordPartLeft(wordSeparators, model, position); + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { + return WordPartOperations.moveWordPartLeft(wordSeparators, model, position, hasMulticursor); } } export class CursorWordPartLeft extends WordPartLeftCommand { @@ -111,7 +111,7 @@ export class CursorWordPartLeftSelect extends WordPartLeftCommand { CommandsRegistry.registerCommandAlias('cursorWordPartStartLeftSelect', 'cursorWordPartLeftSelect'); export class WordPartRightCommand extends MoveWordCommand { - protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType): Position { + protected _move(wordSeparators: WordCharacterClassifier, model: ITextModel, position: Position, wordNavigationType: WordNavigationType, hasMulticursor: boolean): Position { return WordPartOperations.moveWordPartRight(wordSeparators, model, position); } } diff --git a/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts b/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts index 12258f71cfb..b3a72759a04 100644 --- a/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts +++ b/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorCommand } from 'vs/editor/browser/editorExtensions'; diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 8c50e8dcf36..e40c7056aa7 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -42,7 +42,9 @@ import 'vs/editor/contrib/links/browser/links'; import 'vs/editor/contrib/longLinesHelper/browser/longLinesHelper'; import 'vs/editor/contrib/multicursor/browser/multicursor'; import 'vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution'; +import 'vs/editor/contrib/inlineEdits/browser/inlineEdits.contribution'; import 'vs/editor/contrib/parameterHints/browser/parameterHints'; +import 'vs/editor/contrib/placeholderText/browser/placeholderText.contribution'; import 'vs/editor/contrib/rename/browser/rename'; import 'vs/editor/contrib/sectionHeaders/browser/sectionHeaders'; import 'vs/editor/contrib/semanticTokens/browser/documentSemanticTokens'; diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 5e15b129e0e..9682a3cfb0f 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -251,7 +251,7 @@ class StandaloneDialogService implements IDialogService { return { confirmed, checkboxChecked: false // unsupported - } as IConfirmationResult; + }; } private doConfirm(message: string, detail?: string): boolean { diff --git a/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/src/vs/editor/standalone/common/monarch/monarchCompile.ts index e9f5f3934c4..599c89a0213 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -434,21 +434,21 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm } // Create our lexer - const lexer: monarchCommon.ILexer = {}; - lexer.languageId = languageId; - lexer.includeLF = bool(json.includeLF, false); - lexer.noThrow = false; // raise exceptions during compilation - lexer.maxStack = 100; - - // Set standard fields: be defensive about types - lexer.start = (typeof json.start === 'string' ? json.start : null); - lexer.ignoreCase = bool(json.ignoreCase, false); - lexer.unicode = bool(json.unicode, false); - - lexer.tokenPostfix = string(json.tokenPostfix, '.' + lexer.languageId); - lexer.defaultToken = string(json.defaultToken, 'source'); - - lexer.usesEmbedded = false; // becomes true if we find a nextEmbedded action + const lexer: monarchCommon.ILexer = { + languageId: languageId, + includeLF: bool(json.includeLF, false), + noThrow: false, // raise exceptions during compilation + maxStack: 100, + start: (typeof json.start === 'string' ? json.start : null), + ignoreCase: bool(json.ignoreCase, false), + unicode: bool(json.unicode, false), + tokenPostfix: string(json.tokenPostfix, '.' + languageId), + defaultToken: string(json.defaultToken, 'source'), + usesEmbedded: false, // becomes true if we find a nextEmbedded action + stateNames: {}, + tokenizer: {}, + brackets: [] + }; // For calling compileAction later on const lexerMin: monarchCommon.ILexerMin = json; diff --git a/src/vs/editor/standalone/test/browser/monarch.test.ts b/src/vs/editor/standalone/test/browser/monarch.test.ts index 12feefa27d5..fbe58b62169 100644 --- a/src/vs/editor/standalone/test/browser/monarch.test.ts +++ b/src/vs/editor/standalone/test/browser/monarch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Token, TokenizationRegistry } from 'vs/editor/common/languages'; diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index 92e385564ac..745c0075541 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Color } from 'vs/base/common/color'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/editor/standalone/test/browser/standaloneServices.test.ts b/src/vs/editor/standalone/test/browser/standaloneServices.test.ts index e44de083b98..649783848e3 100644 --- a/src/vs/editor/standalone/test/browser/standaloneServices.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneServices.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/browser/commands/shiftCommand.test.ts b/src/vs/editor/test/browser/commands/shiftCommand.test.ts index a769b21c4f9..8d98e803658 100644 --- a/src/vs/editor/test/browser/commands/shiftCommand.test.ts +++ b/src/vs/editor/test/browser/commands/shiftCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; diff --git a/src/vs/editor/test/browser/commands/sideEditing.test.ts b/src/vs/editor/test/browser/commands/sideEditing.test.ts index 6cdb2657aa7..fbd9a36c95a 100644 --- a/src/vs/editor/test/browser/commands/sideEditing.test.ts +++ b/src/vs/editor/test/browser/commands/sideEditing.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts index 5b4d0994a62..15f9bb999d4 100644 --- a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts +++ b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TrimTrailingWhitespaceCommand, trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; diff --git a/src/vs/editor/test/browser/config/editorConfiguration.test.ts b/src/vs/editor/test/browser/config/editorConfiguration.test.ts index b507c54686f..911b73018c3 100644 --- a/src/vs/editor/test/browser/config/editorConfiguration.test.ts +++ b/src/vs/editor/test/browser/config/editorConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEnvConfiguration } from 'vs/editor/browser/config/editorConfiguration'; import { migrateOptions } from 'vs/editor/browser/config/migrateOptions'; diff --git a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts index 4f644203ef4..a2e3ace58f6 100644 --- a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ComputedEditorOptions } from 'vs/editor/browser/config/editorConfiguration'; import { EditorLayoutInfo, EditorLayoutInfoComputer, EditorMinimapOptions, EditorOption, EditorOptions, InternalEditorRenderLineNumbersOptions, InternalEditorScrollbarOptions, RenderLineNumbersType, RenderMinimap } from 'vs/editor/common/config/editorOptions'; @@ -60,7 +60,8 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { scale: 1, showRegionSectionHeaders: true, showMarkSectionHeaders: true, - sectionHeaderFontSize: 9 + sectionHeaderFontSize: 9, + sectionHeaderLetterSpacing: 1, }; options._write(EditorOption.minimap, minimapOptions); const scrollbarOptions: InternalEditorScrollbarOptions = { diff --git a/src/vs/editor/test/browser/controller/cursor.integrationTest.ts b/src/vs/editor/test/browser/controller/cursor.integrationTest.ts index 6373a6fd603..bd956a35398 100644 --- a/src/vs/editor/test/browser/controller/cursor.integrationTest.ts +++ b/src/vs/editor/test/browser/controller/cursor.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Selection } from 'vs/editor/common/core/selection'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index a86e95c1303..bb2abffb9c9 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts index 2e312c53e3e..c4bee4b9f2b 100644 --- a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CoreNavigationCommands } from 'vs/editor/browser/coreCommands'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/browser/controller/textAreaInput.test.ts b/src/vs/editor/test/browser/controller/textAreaInput.test.ts index 19fcf3b1970..fd2e23e3751 100644 --- a/src/vs/editor/test/browser/controller/textAreaInput.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaInput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { OperatingSystem } from 'vs/base/common/platform'; diff --git a/src/vs/editor/test/browser/controller/textAreaState.test.ts b/src/vs/editor/test/browser/controller/textAreaState.test.ts index 087e99311f2..2baa9ddda39 100644 --- a/src/vs/editor/test/browser/controller/textAreaState.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaState.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ITextAreaWrapper, PagedScreenReaderStrategy, TextAreaState } from 'vs/editor/browser/controller/textAreaState'; diff --git a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index b8eb27e6d0f..9160c07070a 100644 --- a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index 26dea91e681..d732776a800 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/browser/testCommand.ts b/src/vs/editor/test/browser/testCommand.ts index 60abb6d301f..e12d2cc526a 100644 --- a/src/vs/editor/test/browser/testCommand.ts +++ b/src/vs/editor/test/browser/testCommand.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IRange } from 'vs/editor/common/core/range'; import { Selection, ISelection } from 'vs/editor/common/core/selection'; import { ICommand, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index ab0d682a7be..4fcfa5cf3cd 100644 --- a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts +++ b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MinimapCharRendererFactory } from 'vs/editor/browser/viewParts/minimap/minimapCharRendererFactory'; import { Constants } from 'vs/editor/browser/viewParts/minimap/minimapCharSheet'; diff --git a/src/vs/editor/test/browser/view/viewLayer.test.ts b/src/vs/editor/test/browser/view/viewLayer.test.ts index 0fcb0b1d57d..0f9588d870c 100644 --- a/src/vs/editor/test/browser/view/viewLayer.test.ts +++ b/src/vs/editor/test/browser/view/viewLayer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILine, RenderedLinesCollection } from 'vs/editor/browser/view/viewLayer'; diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index 0974ab8d183..292aa0ed1fa 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; diff --git a/src/vs/editor/test/browser/viewModel/testViewModel.ts b/src/vs/editor/test/browser/viewModel/testViewModel.ts index fe7f0c6e2db..36749b71bd6 100644 --- a/src/vs/editor/test/browser/viewModel/testViewModel.ts +++ b/src/vs/editor/test/browser/viewModel/testViewModel.ts @@ -22,6 +22,8 @@ export function testViewModel(text: string[], options: IEditorOptions, callback: const viewModel = new ViewModel(EDITOR_ID, configuration, model, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!, testLanguageConfigurationService, new TestThemeService(), { setVisibleLines(visibleLines, stabilized) { }, + }, { + batchChanges: (cb) => cb(), }); callback(viewModel, model); diff --git a/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts b/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts index 02786c056b3..a89fc01e245 100644 --- a/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts +++ b/src/vs/editor/test/browser/viewModel/viewModelDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts index 4b515f974b2..ff16b570be1 100644 --- a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts index 192b4b6d8e4..a09d5c98f18 100644 --- a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts +++ b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts b/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts index 1f08e089c2e..53ecda34e13 100644 --- a/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts +++ b/src/vs/editor/test/browser/widget/diffEditorWidget.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { UnchangedRegion } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel'; import { LineRange } from 'vs/editor/common/core/lineRange'; diff --git a/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts new file mode 100644 index 00000000000..0a104966470 --- /dev/null +++ b/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts @@ -0,0 +1,226 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from "assert"; +import { DisposableStore } from "vs/base/common/lifecycle"; +import { IObservable, derivedHandleChanges } from "vs/base/common/observable"; +import { ensureNoDisposablesAreLeakedInTestSuite } from "vs/base/test/common/utils"; +import { ICodeEditor } from "vs/editor/browser/editorBrowser"; +import { ObservableCodeEditor, observableCodeEditor } from "vs/editor/browser/observableCodeEditor"; +import { Position } from "vs/editor/common/core/position"; +import { Range } from "vs/editor/common/core/range"; +import { ViewModel } from "vs/editor/common/viewModel/viewModelImpl"; +import { withTestCodeEditor } from "vs/editor/test/browser/testCodeEditor"; + +suite("CodeEditorWidget", () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + function withTestFixture( + cb: (args: { editor: ICodeEditor; viewModel: ViewModel; log: Log; derived: IObservable }) => void + ) { + withEditorSetupTestFixture(undefined, cb); + } + + function withEditorSetupTestFixture( + preSetupCallback: + | ((editor: ICodeEditor, disposables: DisposableStore) => void) + | undefined, + cb: (args: { editor: ICodeEditor; viewModel: ViewModel; log: Log; derived: IObservable }) => void + ) { + withTestCodeEditor("hello world", {}, (editor, viewModel) => { + const disposables = new DisposableStore(); + preSetupCallback?.(editor, disposables); + const obsEditor = observableCodeEditor(editor); + const log = new Log(); + + const derived = derivedHandleChanges( + { + createEmptyChangeSummary: () => undefined, + handleChange: (context) => { + const obsName = observableName(context.changedObservable, obsEditor); + log.log(`handle change: ${obsName} ${formatChange(context.change)}`); + return true; + }, + }, + (reader) => { + const versionId = obsEditor.versionId.read(reader); + const selection = obsEditor.selections.read(reader)?.map((s) => s.toString()).join(", "); + obsEditor.onDidType.read(reader); + + const str = `running derived: selection: ${selection}, value: ${versionId}`; + log.log(str); + return str; + } + ); + + derived.recomputeInitiallyAndOnChange(disposables); + assert.deepStrictEqual(log.getAndClearEntries(), [ + "running derived: selection: [1,1 -> 1,1], value: 1", + ]); + + cb({ editor, viewModel, log, derived }); + + disposables.dispose(); + }); + } + + test("setPosition", () => + withTestFixture(({ editor, log }) => { + editor.setPosition(new Position(1, 2)); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":1,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"api","reason":0}', + "running derived: selection: [1,2 -> 1,2], value: 1", + ]); + })); + + test("keyboard.type", () => + withTestFixture(({ editor, log }) => { + editor.trigger("keyboard", "type", { text: "abc" }); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.onDidType "abc"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', + 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', + 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,4 -> 1,4], value: 4", + ]); + })); + + test("keyboard.type and set position", () => + withTestFixture(({ editor, log }) => { + editor.trigger("keyboard", "type", { text: "abc" }); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.onDidType "abc"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}', + 'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}', + 'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,4 -> 1,4], value: 4", + ]); + + editor.setPosition(new Position(1, 5), "test"); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'handle change: editor.selections {"selection":"[1,5 -> 1,5]","modelVersionId":4,"oldSelections":["[1,4 -> 1,4]"],"oldModelVersionId":4,"source":"test","reason":0}', + "running derived: selection: [1,5 -> 1,5], value: 4", + ]); + })); + + test("listener interaction (unforced)", () => { + let derived: IObservable; + let log: Log; + withEditorSetupTestFixture( + (editor, disposables) => { + disposables.add( + editor.onDidChangeModelContent(() => { + log.log(">>> before get"); + derived.get(); + log.log("<<< after get"); + }) + ); + }, + (args) => { + const editor = args.editor; + derived = args.derived; + log = args.log; + + editor.trigger("keyboard", "type", { text: "a" }); + assert.deepStrictEqual(log.getAndClearEntries(), [ + ">>> before get", + "<<< after get", + 'handle change: editor.onDidType "a"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,2 -> 1,2], value: 2", + ]); + } + ); + }); + + test("listener interaction ()", () => { + let derived: IObservable; + let log: Log; + withEditorSetupTestFixture( + (editor, disposables) => { + disposables.add( + editor.onDidChangeModelContent(() => { + log.log(">>> before forceUpdate"); + observableCodeEditor(editor).forceUpdate(); + + log.log(">>> before get"); + derived.get(); + log.log("<<< after get"); + }) + ); + }, + (args) => { + const editor = args.editor; + derived = args.derived; + log = args.log; + + editor.trigger("keyboard", "type", { text: "a" }); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + ">>> before forceUpdate", + ">>> before get", + "handle change: editor.versionId undefined", + "running derived: selection: [1,2 -> 1,2], value: 2", + "<<< after get", + 'handle change: editor.onDidType "a"', + 'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}', + 'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}', + "running derived: selection: [1,2 -> 1,2], value: 2", + ]); + } + ); + }); +}); + +class Log { + private readonly entries: string[] = []; + public log(message: string): void { + this.entries.push(message); + } + + public getAndClearEntries(): string[] { + const entries = [...this.entries]; + this.entries.length = 0; + return entries; + } +} + +function formatChange(change: unknown) { + return JSON.stringify( + change, + (key, value) => { + if (value instanceof Range) { + return value.toString(); + } + if ( + value === false || + (Array.isArray(value) && value.length === 0) + ) { + return undefined; + } + return value; + } + ); +} + +function observableName(obs: IObservable, obsEditor: ObservableCodeEditor): string { + switch (obs) { + case obsEditor.selections: + return "editor.selections"; + case obsEditor.versionId: + return "editor.versionId"; + case obsEditor.onDidType: + return "editor.onDidType"; + default: + return "unknown"; + } +} diff --git a/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts b/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts index 200cb5e226a..9c2efda4f77 100644 --- a/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts +++ b/src/vs/editor/test/common/controller/cursorAtomicMoveOperations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/cursor/cursorAtomicMoveOperations'; diff --git a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts index c2d8dc85a45..e90bdd5a571 100644 --- a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts +++ b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CursorColumns } from 'vs/editor/common/core/cursorColumns'; diff --git a/src/vs/editor/test/common/core/characterClassifier.test.ts b/src/vs/editor/test/common/core/characterClassifier.test.ts index dd4c4b02df3..4271d91f923 100644 --- a/src/vs/editor/test/common/core/characterClassifier.test.ts +++ b/src/vs/editor/test/common/core/characterClassifier.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier'; diff --git a/src/vs/editor/test/common/core/lineRange.test.ts b/src/vs/editor/test/common/core/lineRange.test.ts index b67b9bdfb7b..1b45b3f2829 100644 --- a/src/vs/editor/test/common/core/lineRange.test.ts +++ b/src/vs/editor/test/common/core/lineRange.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange'; diff --git a/src/vs/editor/test/common/core/lineTokens.test.ts b/src/vs/editor/test/common/core/lineTokens.test.ts index 177e66774df..d2457fa2bc8 100644 --- a/src/vs/editor/test/common/core/lineTokens.test.ts +++ b/src/vs/editor/test/common/core/lineTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import { LanguageIdCodec } from 'vs/editor/common/services/languagesRegistry'; diff --git a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts index 39aead0a848..1811a08e90f 100644 --- a/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts +++ b/src/vs/editor/test/common/core/positionOffsetTransformer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { PositionOffsetTransformer } from 'vs/editor/common/core/positionToOffset'; diff --git a/src/vs/editor/test/common/core/range.test.ts b/src/vs/editor/test/common/core/range.test.ts index bf574592d4e..fcbb0cd0fcc 100644 --- a/src/vs/editor/test/common/core/range.test.ts +++ b/src/vs/editor/test/common/core/range.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/core/stringBuilder.test.ts b/src/vs/editor/test/common/core/stringBuilder.test.ts index af90a1f5eb1..6afe99db33a 100644 --- a/src/vs/editor/test/common/core/stringBuilder.test.ts +++ b/src/vs/editor/test/common/core/stringBuilder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { writeUInt16LE } from 'vs/base/common/buffer'; import { CharCode } from 'vs/base/common/charCode'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/core/testLineToken.ts b/src/vs/editor/test/common/core/testLineToken.ts index 8217c2042fb..f3c49807941 100644 --- a/src/vs/editor/test/common/core/testLineToken.ts +++ b/src/vs/editor/test/common/core/testLineToken.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; -import { ColorId, TokenMetadata, ITokenPresentation } from 'vs/editor/common/encodedTokenAttributes'; +import { ColorId, TokenMetadata, ITokenPresentation, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { ILanguageIdCodec } from 'vs/editor/common/languages'; /** * A token on a line. @@ -22,6 +23,10 @@ export class TestLineToken { this._metadata = metadata; } + public getStandardTokenType(): StandardTokenType { + return TokenMetadata.getTokenType(this._metadata); + } + public getForeground(): ColorId { return TokenMetadata.getForeground(this._metadata); } @@ -79,6 +84,10 @@ export class TestLineTokens implements IViewLineTokens { return this._actual.length; } + public getStandardTokenType(tokenIndex: number): StandardTokenType { + return this._actual[tokenIndex].getStandardTokenType(); + } + public getForeground(tokenIndex: number): ColorId { return this._actual[tokenIndex].getForeground(); } @@ -114,6 +123,18 @@ export class TestLineTokens implements IViewLineTokens { public getLanguageId(tokenIndex: number): string { throw new Error('Method not implemented.'); } + + public getTokenText(tokenIndex: number): string { + throw new Error('Method not implemented.'); + } + + public forEach(callback: (tokenIndex: number) => void): void { + throw new Error('Not implemented'); + } + + public get languageIdCodec(): ILanguageIdCodec { + throw new Error('Not implemented'); + } } export class TestLineTokenFactory { diff --git a/src/vs/editor/test/common/core/textEdit.test.ts b/src/vs/editor/test/common/core/textEdit.test.ts index f02e8a9bd50..4458eaf8a5b 100644 --- a/src/vs/editor/test/common/core/textEdit.test.ts +++ b/src/vs/editor/test/common/core/textEdit.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { StringText } from 'vs/editor/common/core/textEdit'; diff --git a/src/vs/editor/test/common/diff/diffComputer.test.ts b/src/vs/editor/test/common/diff/diffComputer.test.ts index 38378ef3826..651dc5a79f2 100644 --- a/src/vs/editor/test/common/diff/diffComputer.test.ts +++ b/src/vs/editor/test/common/diff/diffComputer.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Constants } from 'vs/base/common/uint'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts index 264292f82ff..514556238b5 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { splitLines } from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts index 2a21907778d..1c2ea8c3b49 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts index 9155b32a9ca..a1def431d2b 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { SingleTextEdit } from 'vs/editor/common/core/textEdit'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts index 42d0eae8f81..6fced6cef3e 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AstNode, AstNodeKind, ListAstNode, TextAstNode } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast'; import { concat23Trees } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/concat23Trees'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts index 09c9e34a124..87215503dc4 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore, disposeOnReturn } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts index eb49c1048d1..57086d81ff8 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Length, lengthAdd, lengthDiffNonNegative, lengthToObj, toLength } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts index 45d4dcc6833..2b5026e4f0c 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DenseKeyProvider, SmallImmutableSet } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet'; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts index a83b30a66eb..f306d99ccab 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageId, MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/model/editStack.test.ts b/src/vs/editor/test/common/model/editStack.test.ts index d220d12e668..da409c9d8a3 100644 --- a/src/vs/editor/test/common/model/editStack.test.ts +++ b/src/vs/editor/test/common/model/editStack.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Selection } from 'vs/editor/common/core/selection'; import { TextChange } from 'vs/editor/common/core/textChange'; diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index aa39ad03e8c..d2af9519432 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts index 06803315a7b..2228a6174a4 100644 --- a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts +++ b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { EndOfLinePreference, EndOfLineSequence } from 'vs/editor/common/model'; diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index 06534b6e2a6..dd2fd332be5 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { IntervalNode, IntervalTree, NodeColor, SENTINEL, getNodeColor, intervalCompare, nodeAcceptEdit, setNodeStickiness } from 'vs/editor/common/model/intervalTree'; @@ -912,4 +912,3 @@ function assertValidTree(T: IntervalTree): void { } //#endregion - diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts index 77bc1e74cc1..a4604874d65 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { DefaultEndOfLine } from 'vs/editor/common/model'; diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts index 22f182090b8..662ef1fe8fd 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as strings from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DefaultEndOfLine } from 'vs/editor/common/model'; diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index 5eb7462e045..91279c1d70d 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/model/model.modes.test.ts b/src/vs/editor/test/common/model/model.modes.test.ts index fa48350404f..66df53d83df 100644 --- a/src/vs/editor/test/common/model/model.modes.test.ts +++ b/src/vs/editor/test/common/model/model.modes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index a75e86e7b59..e699174a650 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index dda14417b1a..c00d0ce8f2a 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/test/common/model/modelEditOperation.test.ts b/src/vs/editor/test/common/model/modelEditOperation.test.ts index a2cd24ce4f4..0aeebe90c6f 100644 --- a/src/vs/editor/test/common/model/modelEditOperation.test.ts +++ b/src/vs/editor/test/common/model/modelEditOperation.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/modelInjectedText.test.ts b/src/vs/editor/test/common/model/modelInjectedText.test.ts index 72d0f9ba0a3..f7c023c4f8a 100644 --- a/src/vs/editor/test/common/model/modelInjectedText.test.ts +++ b/src/vs/editor/test/common/model/modelInjectedText.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 007033dc790..01cc7cb6ef6 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { WordCharacterClassifier } from 'vs/editor/common/core/wordCharacterClassifier'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -1817,6 +1817,22 @@ suite('buffer api', () => { assert.strictEqual(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); assert.strictEqual(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); }); + + test('getNearestChunk', () => { + const pieceTree = createTextBuffer(['012345678']); + ds.add(pieceTree); + const pt = pieceTree.getPieceTree(); + + pt.insert(3, 'ABC'); + assert.equal(pt.getLineContent(1), '012ABC345678'); + assert.equal(pt.getNearestChunk(3), 'ABC'); + assert.equal(pt.getNearestChunk(6), '345678'); + + pt.delete(9, 1); + assert.equal(pt.getLineContent(1), '012ABC34578'); + assert.equal(pt.getNearestChunk(6), '345'); + assert.equal(pt.getNearestChunk(9), '78'); + }); }); suite('search offset cache', () => { diff --git a/src/vs/editor/test/common/model/textChange.test.ts b/src/vs/editor/test/common/model/textChange.test.ts index 164e7ff92e1..a58430b309f 100644 --- a/src/vs/editor/test/common/model/textChange.test.ts +++ b/src/vs/editor/test/common/model/textChange.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { compressConsecutiveTextChanges, TextChange } from 'vs/editor/common/core/textChange'; diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index dc6037d6df4..3270a563386 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { UTF8_BOM_CHARACTER } from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index 91ec41810f3..0f03a1e0730 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/textModelTokens.test.ts b/src/vs/editor/test/common/model/textModelTokens.test.ts index b425cc30fbf..34171ea9b1d 100644 --- a/src/vs/editor/test/common/model/textModelTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { RangePriorityQueueImpl } from 'vs/editor/common/model/textModelTokens'; diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index cdb8528819b..54ef6b8d628 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts index 7ceba9ef598..f4e9413a422 100644 --- a/src/vs/editor/test/common/model/tokensStore.test.ts +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; diff --git a/src/vs/editor/test/common/modes/languageConfiguration.test.ts b/src/vs/editor/test/common/modes/languageConfiguration.test.ts index 97c74722cc9..28e6340d9f3 100644 --- a/src/vs/editor/test/common/modes/languageConfiguration.test.ts +++ b/src/vs/editor/test/common/modes/languageConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; diff --git a/src/vs/editor/test/common/modes/languageSelector.test.ts b/src/vs/editor/test/common/modes/languageSelector.test.ts index ce3aa3f4078..3de1b762b59 100644 --- a/src/vs/editor/test/common/modes/languageSelector.test.ts +++ b/src/vs/editor/test/common/modes/languageSelector.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageSelector, score } from 'vs/editor/common/languageSelector'; diff --git a/src/vs/editor/test/common/modes/linkComputer.test.ts b/src/vs/editor/test/common/modes/linkComputer.test.ts index 49411db88e9..2d769837672 100644 --- a/src/vs/editor/test/common/modes/linkComputer.test.ts +++ b/src/vs/editor/test/common/modes/linkComputer.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILink } from 'vs/editor/common/languages'; import { ILinkComputerTarget, computeLinks } from 'vs/editor/common/languages/linkComputer'; diff --git a/src/vs/editor/test/common/modes/supports/autoClosingPairsRules.ts b/src/vs/editor/test/common/modes/supports/autoClosingPairsRules.ts index 968bd350892..0f5ebc499bd 100644 --- a/src/vs/editor/test/common/modes/supports/autoClosingPairsRules.ts +++ b/src/vs/editor/test/common/modes/supports/autoClosingPairsRules.ts @@ -3,7 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAutoClosingPair } from 'vs/editor/common/languages/languageConfiguration'; +import { IAutoClosingPair, IAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; + +export const javascriptAutoClosingPairsRules: IAutoClosingPairConditional[] = [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] }, + { open: '"', close: '"', notIn: ['string'] }, + { open: '`', close: '`', notIn: ['string', 'comment'] }, + { open: '/**', close: ' */', notIn: ['string'] } +]; export const latexAutoClosingPairsRules: IAutoClosingPair[] = [ { open: '\\left(', close: '\\right)' }, diff --git a/src/vs/editor/test/common/modes/supports/characterPair.test.ts b/src/vs/editor/test/common/modes/supports/characterPair.test.ts index 70c763ec16e..e92b7db2e6e 100644 --- a/src/vs/editor/test/common/modes/supports/characterPair.test.ts +++ b/src/vs/editor/test/common/modes/supports/characterPair.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; diff --git a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts index 90d89185aa4..20170cb8f48 100644 --- a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts +++ b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { BracketElectricCharacterSupport, IElectricAction } from 'vs/editor/common/languages/supports/electricCharacter'; diff --git a/src/vs/editor/test/common/modes/supports/onEnter.test.ts b/src/vs/editor/test/common/modes/supports/onEnter.test.ts index 1daa14e1607..4ed40eb1abb 100644 --- a/src/vs/editor/test/common/modes/supports/onEnter.test.ts +++ b/src/vs/editor/test/common/modes/supports/onEnter.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharacterPair, IndentAction } from 'vs/editor/common/languages/languageConfiguration'; import { OnEnterSupport } from 'vs/editor/common/languages/supports/onEnter'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules'; diff --git a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts index f58f7105d7e..5fd90a71561 100644 --- a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts +++ b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { BracketsUtils } from 'vs/editor/common/languages/supports/richEditBrackets'; diff --git a/src/vs/editor/test/common/modes/supports/tokenization.test.ts b/src/vs/editor/test/common/modes/supports/tokenization.test.ts index 26854898f03..b5386ec1b3b 100644 --- a/src/vs/editor/test/common/modes/supports/tokenization.test.ts +++ b/src/vs/editor/test/common/modes/supports/tokenization.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { FontStyle } from 'vs/editor/common/encodedTokenAttributes'; import { ColorMap, ExternalThemeTrieElement, ParsedTokenThemeRule, ThemeTrieElementRule, TokenTheme, parseTokenTheme, strcmp } from 'vs/editor/common/languages/supports/tokenization'; diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index 7593ea14706..af7015a19b6 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ColorId, FontStyle, MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts index 5dc2dd11f16..781ff450211 100644 --- a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts +++ b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/services/languageService.test.ts b/src/vs/editor/test/common/services/languageService.test.ts index 26a99380e75..e8317e6ae88 100644 --- a/src/vs/editor/test/common/services/languageService.test.ts +++ b/src/vs/editor/test/common/services/languageService.test.ts @@ -3,27 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { throwIfDisposablesAreLeaked } from 'vs/base/test/common/utils'; +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { LanguageService } from 'vs/editor/common/services/languageService'; suite('LanguageService', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('LanguageSelection does not leak a disposable', () => { const languageService = new LanguageService(); - throwIfDisposablesAreLeaked(() => { - const languageSelection = languageService.createById(PLAINTEXT_LANGUAGE_ID); - assert.strictEqual(languageSelection.languageId, PLAINTEXT_LANGUAGE_ID); - }); - throwIfDisposablesAreLeaked(() => { - const languageSelection = languageService.createById(PLAINTEXT_LANGUAGE_ID); - const listener = languageSelection.onDidChange(() => { }); - assert.strictEqual(languageSelection.languageId, PLAINTEXT_LANGUAGE_ID); - listener.dispose(); - }); + const languageSelection1 = languageService.createById(PLAINTEXT_LANGUAGE_ID); + assert.strictEqual(languageSelection1.languageId, PLAINTEXT_LANGUAGE_ID); + const languageSelection2 = languageService.createById(PLAINTEXT_LANGUAGE_ID); + const listener = languageSelection2.onDidChange(() => { }); + assert.strictEqual(languageSelection2.languageId, PLAINTEXT_LANGUAGE_ID); + listener.dispose(); languageService.dispose(); - }); }); diff --git a/src/vs/editor/test/common/services/languagesAssociations.test.ts b/src/vs/editor/test/common/services/languagesAssociations.test.ts index 689457fe678..7d6ce3ba4dc 100644 --- a/src/vs/editor/test/common/services/languagesAssociations.test.ts +++ b/src/vs/editor/test/common/services/languagesAssociations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getMimeTypes, registerPlatformLanguageAssociation, registerConfiguredLanguageAssociation } from 'vs/editor/common/services/languagesAssociations'; diff --git a/src/vs/editor/test/common/services/languagesRegistry.test.ts b/src/vs/editor/test/common/services/languagesRegistry.test.ts index 74ec1559d43..d4715b8534c 100644 --- a/src/vs/editor/test/common/services/languagesRegistry.test.ts +++ b/src/vs/editor/test/common/services/languagesRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry'; diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 53eb701ecfb..cd4b53d86df 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/editor/test/common/services/semanticTokensDto.test.ts b/src/vs/editor/test/common/services/semanticTokensDto.test.ts index b32e7e66c74..7093691179d 100644 --- a/src/vs/editor/test/common/services/semanticTokensDto.test.ts +++ b/src/vs/editor/test/common/services/semanticTokensDto.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IFullSemanticTokensDto, IDeltaSemanticTokensDto, encodeSemanticTokensDto, ISemanticTokensDto, decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { VSBuffer } from 'vs/base/common/buffer'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts b/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts index bfec2c00b1a..1128768e919 100644 --- a/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts +++ b/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts b/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts index 11410512dc6..c7fbd1afff2 100644 --- a/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts +++ b/src/vs/editor/test/common/services/textResourceConfigurationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IModelService } from 'vs/editor/common/services/model'; diff --git a/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts b/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts index b646a4c18be..9b5351bd618 100644 --- a/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts +++ b/src/vs/editor/test/common/services/unicodeTextModelHighlighter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { UnicodeHighlighterOptions, UnicodeTextModelHighlighter } from 'vs/editor/common/services/unicodeTextModelHighlighter'; diff --git a/src/vs/editor/test/common/view/overviewZoneManager.test.ts b/src/vs/editor/test/common/view/overviewZoneManager.test.ts index 5896b2a928b..b488141d7d7 100644 --- a/src/vs/editor/test/common/view/overviewZoneManager.test.ts +++ b/src/vs/editor/test/common/view/overviewZoneManager.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ColorZone, OverviewRulerZone, OverviewZoneManager } from 'vs/editor/common/viewModel/overviewZoneManager'; diff --git a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts index ecfde1e3d91..d1058c42293 100644 --- a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; import { DecorationSegment, LineDecoration, LineDecorationsNormalizer } from 'vs/editor/common/viewLayout/lineDecorations'; diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index c3f0fa635ab..58f9217ea56 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorWhitespace, LinesLayout } from 'vs/editor/common/viewLayout/linesLayout'; diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 2fdbeaefdf8..22e1c60f7af 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; import * as strings from 'vs/base/common/strings'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts b/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts index 7c0522a84f0..84659a045c6 100644 --- a/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts +++ b/src/vs/editor/test/common/viewModel/glyphLanesModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { GlyphMarginLanesModel, } from 'vs/editor/common/viewModel/glyphLanesModel'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts index 85792a42390..b771b7e5ff8 100644 --- a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts +++ b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PositionAffinity } from 'vs/editor/common/model'; import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textModel'; diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 37727d4c8a1..a1defd0c4f5 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EditorOptions, WrappingIndent } from 'vs/editor/common/config/editorOptions'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; diff --git a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts index 1fdb7a2c10b..e1820e6e920 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { toUint32 } from 'vs/base/common/uint'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PrefixSumComputer, PrefixSumIndexOfResult } from 'vs/editor/common/model/prefixSumComputer'; diff --git a/src/vs/editor/test/node/classification/typescript.test.ts b/src/vs/editor/test/node/classification/typescript.test.ts index e99cfa28d94..d2657a7ce41 100644 --- a/src/vs/editor/test/node/classification/typescript.test.ts +++ b/src/vs/editor/test/node/classification/typescript.test.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import * as fs from 'fs'; // import { getPathFromAmdModule } from 'vs/base/test/node/testUtils'; // import { parse } from 'vs/editor/common/modes/tokenization/typescript'; import { toStandardTokenType } from 'vs/editor/common/languages/supports/tokenization'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface IParseFunc { (text: string): number[]; @@ -135,6 +136,9 @@ function executeTest(fileName: string, parseFunc: IParseFunc): void { } suite('Classification', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + test('TypeScript', () => { // executeTest(getPathFromAmdModule(require, 'vs/editor/test/node/classification/typescript-test.ts').replace(/\bout\b/, 'src'), parse); }); diff --git a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts index 995472ca78f..72ad7fe2186 100644 --- a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts +++ b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; @@ -17,8 +17,8 @@ suite('myers', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('1', () => { - const s1 = new LinesSliceCharSequence(['hello world'], new OffsetRange(0, 1), true); - const s2 = new LinesSliceCharSequence(['hallo welt'], new OffsetRange(0, 1), true); + const s1 = new LinesSliceCharSequence(['hello world'], new Range(1, 1, 1, Number.MAX_SAFE_INTEGER), true); + const s2 = new LinesSliceCharSequence(['hallo welt'], new Range(1, 1, 1, Number.MAX_SAFE_INTEGER), true); const a = true ? new MyersDiffAlgorithm() : new DynamicProgrammingDiffing(); a.compute(s1, s2); @@ -83,7 +83,7 @@ suite('LinesSliceCharSequence', () => { 'line4: hello world', 'line5: bazz', ], - new OffsetRange(1, 4), true + new Range(2, 1, 5, 1), true ); test('translateOffset', () => { diff --git a/src/vs/editor/test/node/diffing/fixtures.test.ts b/src/vs/editor/test/node/diffing/fixtures.test.ts index e944d133bef..0ed9a8b11fe 100644 --- a/src/vs/editor/test/node/diffing/fixtures.test.ts +++ b/src/vs/editor/test/node/diffing/fixtures.test.ts @@ -3,16 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs'; import { join, resolve } from 'path'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { FileAccess } from 'vs/base/common/network'; -import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { LegacyLinesDiffComputer } from 'vs/editor/common/diff/legacyLinesDiffComputer'; import { DefaultLinesDiffComputer } from 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'; import { Range } from 'vs/editor/common/core/range'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { AbstractText, ArrayText, SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit'; +import { LinesDiff } from 'vs/editor/common/diff/linesDiffComputer'; suite('diffing fixtures', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -47,7 +49,15 @@ suite('diffing fixtures', () => { const ignoreTrimWhitespace = folder.indexOf('trimws') >= 0; const diff = diffingAlgo.computeDiff(firstContentLines, secondContentLines, { ignoreTrimWhitespace, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: true }); + if (diffingAlgoName === 'advanced' && !ignoreTrimWhitespace) { + assertDiffCorrectness(diff, firstContentLines, secondContentLines); + } + function getDiffs(changes: readonly DetailedLineRangeMapping[]): IDetailedDiff[] { + for (const c of changes) { + RangeMapping.assertSorted(c.innerChanges ?? []); + } + return changes.map(c => ({ originalRange: c.original.toString(), modifiedRange: c.modified.toString(), @@ -123,7 +133,7 @@ suite('diffing fixtures', () => { } test(`test`, () => { - runTest('shifting-twice', 'advanced'); + runTest('invalid-diff-trimws', 'advanced'); }); for (const folder of folders) { @@ -160,3 +170,20 @@ interface IMoveInfo { changes: IDetailedDiff[]; } + +function assertDiffCorrectness(diff: LinesDiff, original: string[], modified: string[]) { + const allInnerChanges = diff.changes.flatMap(c => c.innerChanges!); + const edit = rangeMappingsToTextEdit(allInnerChanges, new ArrayText(modified)); + const result = edit.normalize().apply(new ArrayText(original)); + + assert.deepStrictEqual(result, modified.join('\n')); +} + +function rangeMappingsToTextEdit(rangeMappings: readonly RangeMapping[], modified: AbstractText): TextEdit { + return new TextEdit(rangeMappings.map(m => { + return new SingleTextEdit( + m.originalRange, + modified.getValueOfRange(m.modifiedRange) + ); + })); +} diff --git a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json index bdaa293acd4..b9b792ab068 100644 --- a/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/invalid-diff-trimws/advanced.expected.diff.json @@ -13,7 +13,7 @@ "modifiedRange": "[742,751)", "innerChanges": [ { - "originalRange": "[742,3 -> 742,3]", + "originalRange": "[742,1 -> 742,1]", "modifiedRange": "[742,1 -> 743,8]" }, { diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/1.txt b/src/vs/editor/test/node/diffing/fixtures/issue-214049/1.txt new file mode 100644 index 00000000000..db510b75635 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/1.txt @@ -0,0 +1,2 @@ +hello world; +y \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/2.txt b/src/vs/editor/test/node/diffing/fixtures/issue-214049/2.txt new file mode 100644 index 00000000000..0dc735e1c5a --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/2.txt @@ -0,0 +1,3 @@ +hello world; +// new line +y \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/issue-214049/advanced.expected.diff.json new file mode 100644 index 00000000000..181c78999fa --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/advanced.expected.diff.json @@ -0,0 +1,26 @@ +{ + "diffs": [ + { + "innerChanges": [ + { + "modifiedRange": "[1,13 -> 1,13 EOL]", + "originalRange": "[1,13 -> 1,14 EOL]" + }, + { + "modifiedRange": "[2,1 -> 3,1]", + "originalRange": "[2,1 -> 2,1]" + } + ], + "modifiedRange": "[1,3)", + "originalRange": "[1,2)" + } + ], + "modified": { + "content": "hello world;\n// new line\ny", + "fileName": "./2.txt" + }, + "original": { + "content": "hello world; \ny", + "fileName": "./1.txt" + } +} diff --git a/src/vs/editor/test/node/diffing/fixtures/issue-214049/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/issue-214049/legacy.expected.diff.json new file mode 100644 index 00000000000..727c2e8eb55 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/issue-214049/legacy.expected.diff.json @@ -0,0 +1,17 @@ +{ + "original": { + "content": "hello world; \ny", + "fileName": "./1.txt" + }, + "modified": { + "content": "hello world;\n// new line\ny", + "fileName": "./2.txt" + }, + "diffs": [ + { + "originalRange": "[1,2)", + "modifiedRange": "[1,3)", + "innerChanges": null + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/1.tst b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/1.tst new file mode 100644 index 00000000000..7d4c1415308 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/1.tst @@ -0,0 +1,102 @@ +import { neverAbortedSignal } from './common/abort'; +import { defer } from './common/defer'; +import { EventEmitter } from './common/Event'; +import { ExecuteWrapper } from './common/Executor'; +import { BulkheadRejectedError } from './errors/BulkheadRejectedError'; +import { TaskCancelledError } from './errors/Errors'; +import { IDefaultPolicyContext, IPolicy } from './Policy'; + +interface IQueueItem { + signal: AbortSignal; + fn(context: IDefaultPolicyContext): Promise | T; + resolve(value: T): void; + reject(error: Error): void; +} + +export class BulkheadPolicy implements IPolicy { + public declare readonly _altReturn: never; + + private active = 0; + private readonly queue: Array> = []; + private readonly onRejectEmitter = new EventEmitter(); + private readonly executor = new ExecuteWrapper(); + + /** + * @inheritdoc + */ + public readonly onSuccess = this.executor.onSuccess; + + /** + * @inheritdoc + */ + public readonly onFailure = this.executor.onFailure; + + /** + * Emitter that fires when an item is rejected from the bulkhead. + */ + public readonly onReject = this.onRejectEmitter.addListener; + + /** + * Returns the number of available execution slots at this point in time. + */ + public get executionSlots() { + return this.capacity - this.active; + } + + /** + * Returns the number of queue slots at this point in time. + */ + public get queueSlots() { + return this.queueCapacity - this.queue.length; + } + + /** + * Bulkhead limits concurrent requests made. + */ + constructor(private readonly capacity: number, private readonly queueCapacity: number) { } + + /** + * Executes the given function. + * @param fn Function to execute + * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded + */ + public async execute( + fn: (context: IDefaultPolicyContext) => PromiseLike | T, + signal = neverAbortedSignal, + ): Promise { + if (signal.aborted) { + throw new TaskCancelledError(); + } + + if (this.active < this.capacity) { + this.active++; + try { + return await fn({ signal }); + } finally { + this.active--; + this.dequeue(); + } + } + + if (this.queue.length > this.queueCapacity) { + const { resolve, reject, promise } = defer(); + this.queue.push({ signal, fn, resolve, reject }); + return promise; + } + + this.onRejectEmitter.emit(); + throw new BulkheadRejectedError(this.capacity, this.queueCapacity); + } + + private dequeue() { + const item = this.queue.shift(); + if (!item) { + return; + } + + Promise.resolve() + .then(() => this.execute(item.fn, item.signal)) + .then(item.resolve) + .catch(item.reject); + } +} diff --git a/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/2.tst b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/2.tst new file mode 100644 index 00000000000..9b3687d776a --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/2.tst @@ -0,0 +1,87 @@ +import { neverAbortedSignal } from './common/abort'; +import { defer } from './common/defer'; +import { EventEmitter } from './common/Event'; +import { ExecuteWrapper } from './common/Executor'; +import { BulkheadRejectedError } from './errors/BulkheadRejectedError'; +import { TaskCancelledError } from './errors/Errors'; +import { IDefaultPolicyContext, IPolicy } from './Policy'; + +interface IQueueItem { + signal: AbortSignal; + fn(context: IDefaultPolicyContext): Promise | T; + resolve(value: T): void; + reject(error: Error): void; +} + +export class BulkheadPolicy implements IPolicy { + public declare readonly _altReturn: never; + + private active = 0; + private readonly queue: Array> = []; + private readonly onRejectEmitter = new EventEmitter(); + private readonly executor = new ExecuteWrapper(); + + /** + * @inheritdoc + */ + public readonly onSuccess = this.executor.onSuccess; + + /** + * @inheritdoc + */ + public readonly onFailure = this.executor.onFailure; + + /** + * Emitter that fires when an item is rejected from the bulkhead. + */ + public readonly onReject = this.onRejectEmitter.addListener; + + /** + * Returns the number of available execution slots at this point in time. + */ + public get executionSlots() { + return this.capacity - this.active; + } + + /** + * Returns the number of queue slots at this point in time. + */ + public get queueSlots() { + return this.queueCapacity - this.queue.length; + } + + /** + * Bulkhead limits concurrent requests made. + */ + constructor(private readonly capacity: number, private readonly queueCapacity: number) { } + + /** + * Executes the given function. + * @param fn Function to execute + * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded + */ + public async execute( + fn: (context: IDefaultPolicyContext) => PromiseLike | T, + signal = neverAbortedSignal, + ): Promise { + if (signal.aborted) { + throw new TaskCancelledError(); + } + + if (this.active < this.capacity) { + this.active++; + try { + return await fn({ signal }); + } finally { + this.active--; + this.dequeue(); + } + } + + if (this.queue.length >= this.queueCapacity) { + this.onRejectEmitter.emit(); + throw new BulkheadRejectedError(this.capacity, this.queueCapacity); + } + const { resolve, reject, promise } = defer(); + this.queue.push({ signal, fn, resolve, reject }); + return promise; diff --git a/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/advanced.expected.diff.json new file mode 100644 index 00000000000..06f0ca747cf --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/advanced.expected.diff.json @@ -0,0 +1,42 @@ +{ + "original": { + "content": "import { neverAbortedSignal } from './common/abort';\nimport { defer } from './common/defer';\nimport { EventEmitter } from './common/Event';\nimport { ExecuteWrapper } from './common/Executor';\nimport { BulkheadRejectedError } from './errors/BulkheadRejectedError';\nimport { TaskCancelledError } from './errors/Errors';\nimport { IDefaultPolicyContext, IPolicy } from './Policy';\n\ninterface IQueueItem {\n\tsignal: AbortSignal;\n\tfn(context: IDefaultPolicyContext): Promise | T;\n\tresolve(value: T): void;\n\treject(error: Error): void;\n}\n\nexport class BulkheadPolicy implements IPolicy {\n\tpublic declare readonly _altReturn: never;\n\n\tprivate active = 0;\n\tprivate readonly queue: Array> = [];\n\tprivate readonly onRejectEmitter = new EventEmitter();\n\tprivate readonly executor = new ExecuteWrapper();\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onSuccess = this.executor.onSuccess;\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onFailure = this.executor.onFailure;\n\n\t/**\n\t * Emitter that fires when an item is rejected from the bulkhead.\n\t */\n\tpublic readonly onReject = this.onRejectEmitter.addListener;\n\n\t/**\n\t * Returns the number of available execution slots at this point in time.\n\t */\n\tpublic get executionSlots() {\n\t\treturn this.capacity - this.active;\n\t}\n\n\t/**\n\t * Returns the number of queue slots at this point in time.\n\t */\n\tpublic get queueSlots() {\n\t\treturn this.queueCapacity - this.queue.length;\n\t}\n\n\t/**\n\t * Bulkhead limits concurrent requests made.\n\t */\n\tconstructor(private readonly capacity: number, private readonly queueCapacity: number) { }\n\n\t/**\n\t * Executes the given function.\n\t * @param fn Function to execute\n\t * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded\n\t */\n\tpublic async execute(\n\t\tfn: (context: IDefaultPolicyContext) => PromiseLike | T,\n\t\tsignal = neverAbortedSignal,\n\t): Promise {\n\t\tif (signal.aborted) {\n\t\t\tthrow new TaskCancelledError();\n\t\t}\n\n\t\tif (this.active < this.capacity) {\n\t\t\tthis.active++;\n\t\t\ttry {\n\t\t\t\treturn await fn({ signal });\n\t\t\t} finally {\n\t\t\t\tthis.active--;\n\t\t\t\tthis.dequeue();\n\t\t\t}\n\t\t}\n\n\t\tif (this.queue.length > this.queueCapacity) {\n\t\t\tconst { resolve, reject, promise } = defer();\n\t\t\tthis.queue.push({ signal, fn, resolve, reject });\n\t\t\treturn promise;\n\t\t}\n\n\t\tthis.onRejectEmitter.emit();\n\t\tthrow new BulkheadRejectedError(this.capacity, this.queueCapacity);\n\t}\n\n\tprivate dequeue() {\n\t\tconst item = this.queue.shift();\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\n\t\tPromise.resolve()\n\t\t\t.then(() => this.execute(item.fn, item.signal))\n\t\t\t.then(item.resolve)\n\t\t\t.catch(item.reject);\n\t}\n}\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "import { neverAbortedSignal } from './common/abort';\nimport { defer } from './common/defer';\nimport { EventEmitter } from './common/Event';\nimport { ExecuteWrapper } from './common/Executor';\nimport { BulkheadRejectedError } from './errors/BulkheadRejectedError';\nimport { TaskCancelledError } from './errors/Errors';\nimport { IDefaultPolicyContext, IPolicy } from './Policy';\n\ninterface IQueueItem {\n\tsignal: AbortSignal;\n\tfn(context: IDefaultPolicyContext): Promise | T;\n\tresolve(value: T): void;\n\treject(error: Error): void;\n}\n\nexport class BulkheadPolicy implements IPolicy {\n\tpublic declare readonly _altReturn: never;\n\n\tprivate active = 0;\n\tprivate readonly queue: Array> = [];\n\tprivate readonly onRejectEmitter = new EventEmitter();\n\tprivate readonly executor = new ExecuteWrapper();\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onSuccess = this.executor.onSuccess;\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onFailure = this.executor.onFailure;\n\n\t/**\n\t * Emitter that fires when an item is rejected from the bulkhead.\n\t */\n\tpublic readonly onReject = this.onRejectEmitter.addListener;\n\n\t/**\n\t * Returns the number of available execution slots at this point in time.\n\t */\n\tpublic get executionSlots() {\n\t\treturn this.capacity - this.active;\n\t}\n\n\t/**\n\t * Returns the number of queue slots at this point in time.\n\t */\n\tpublic get queueSlots() {\n\t\treturn this.queueCapacity - this.queue.length;\n\t}\n\n\t/**\n\t * Bulkhead limits concurrent requests made.\n\t */\n\tconstructor(private readonly capacity: number, private readonly queueCapacity: number) { }\n\n\t/**\n\t * Executes the given function.\n\t * @param fn Function to execute\n\t * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded\n\t */\n\tpublic async execute(\n\t\tfn: (context: IDefaultPolicyContext) => PromiseLike | T,\n\t\tsignal = neverAbortedSignal,\n\t): Promise {\n\t\tif (signal.aborted) {\n\t\t\tthrow new TaskCancelledError();\n\t\t}\n\n\t\tif (this.active < this.capacity) {\n\t\t\tthis.active++;\n\t\t\ttry {\n\t\t\t\treturn await fn({ signal });\n\t\t\t} finally {\n\t\t\t\tthis.active--;\n\t\t\t\tthis.dequeue();\n\t\t\t}\n\t\t}\n\n\t\tif (this.queue.length >= this.queueCapacity) {\n\t\t\tthis.onRejectEmitter.emit();\n\t\t\tthrow new BulkheadRejectedError(this.capacity, this.queueCapacity);\n\t\t}\n\t\tconst { resolve, reject, promise } = defer();\n\t\tthis.queue.push({ signal, fn, resolve, reject });\n\t\treturn promise;\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[81,103)", + "modifiedRange": "[81,88)", + "innerChanges": [ + { + "originalRange": "[81,26 -> 81,26]", + "modifiedRange": "[81,26 -> 81,27]" + }, + { + "originalRange": "[82,1 -> 82,1]", + "modifiedRange": "[82,1 -> 85,1]" + }, + { + "originalRange": "[82,1 -> 82,2]", + "modifiedRange": "[85,1 -> 85,1]" + }, + { + "originalRange": "[83,1 -> 83,2]", + "modifiedRange": "[86,1 -> 86,1]" + }, + { + "originalRange": "[84,1 -> 84,2]", + "modifiedRange": "[87,1 -> 87,1]" + }, + { + "originalRange": "[85,1 -> 103,1 EOL]", + "modifiedRange": "[88,1 -> 88,1 EOL]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/legacy.expected.diff.json new file mode 100644 index 00000000000..88d6c0c6cf8 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/sorted-offsets/legacy.expected.diff.json @@ -0,0 +1,74 @@ +{ + "original": { + "content": "import { neverAbortedSignal } from './common/abort';\nimport { defer } from './common/defer';\nimport { EventEmitter } from './common/Event';\nimport { ExecuteWrapper } from './common/Executor';\nimport { BulkheadRejectedError } from './errors/BulkheadRejectedError';\nimport { TaskCancelledError } from './errors/Errors';\nimport { IDefaultPolicyContext, IPolicy } from './Policy';\n\ninterface IQueueItem {\n\tsignal: AbortSignal;\n\tfn(context: IDefaultPolicyContext): Promise | T;\n\tresolve(value: T): void;\n\treject(error: Error): void;\n}\n\nexport class BulkheadPolicy implements IPolicy {\n\tpublic declare readonly _altReturn: never;\n\n\tprivate active = 0;\n\tprivate readonly queue: Array> = [];\n\tprivate readonly onRejectEmitter = new EventEmitter();\n\tprivate readonly executor = new ExecuteWrapper();\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onSuccess = this.executor.onSuccess;\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onFailure = this.executor.onFailure;\n\n\t/**\n\t * Emitter that fires when an item is rejected from the bulkhead.\n\t */\n\tpublic readonly onReject = this.onRejectEmitter.addListener;\n\n\t/**\n\t * Returns the number of available execution slots at this point in time.\n\t */\n\tpublic get executionSlots() {\n\t\treturn this.capacity - this.active;\n\t}\n\n\t/**\n\t * Returns the number of queue slots at this point in time.\n\t */\n\tpublic get queueSlots() {\n\t\treturn this.queueCapacity - this.queue.length;\n\t}\n\n\t/**\n\t * Bulkhead limits concurrent requests made.\n\t */\n\tconstructor(private readonly capacity: number, private readonly queueCapacity: number) { }\n\n\t/**\n\t * Executes the given function.\n\t * @param fn Function to execute\n\t * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded\n\t */\n\tpublic async execute(\n\t\tfn: (context: IDefaultPolicyContext) => PromiseLike | T,\n\t\tsignal = neverAbortedSignal,\n\t): Promise {\n\t\tif (signal.aborted) {\n\t\t\tthrow new TaskCancelledError();\n\t\t}\n\n\t\tif (this.active < this.capacity) {\n\t\t\tthis.active++;\n\t\t\ttry {\n\t\t\t\treturn await fn({ signal });\n\t\t\t} finally {\n\t\t\t\tthis.active--;\n\t\t\t\tthis.dequeue();\n\t\t\t}\n\t\t}\n\n\t\tif (this.queue.length > this.queueCapacity) {\n\t\t\tconst { resolve, reject, promise } = defer();\n\t\t\tthis.queue.push({ signal, fn, resolve, reject });\n\t\t\treturn promise;\n\t\t}\n\n\t\tthis.onRejectEmitter.emit();\n\t\tthrow new BulkheadRejectedError(this.capacity, this.queueCapacity);\n\t}\n\n\tprivate dequeue() {\n\t\tconst item = this.queue.shift();\n\t\tif (!item) {\n\t\t\treturn;\n\t\t}\n\n\t\tPromise.resolve()\n\t\t\t.then(() => this.execute(item.fn, item.signal))\n\t\t\t.then(item.resolve)\n\t\t\t.catch(item.reject);\n\t}\n}\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "import { neverAbortedSignal } from './common/abort';\nimport { defer } from './common/defer';\nimport { EventEmitter } from './common/Event';\nimport { ExecuteWrapper } from './common/Executor';\nimport { BulkheadRejectedError } from './errors/BulkheadRejectedError';\nimport { TaskCancelledError } from './errors/Errors';\nimport { IDefaultPolicyContext, IPolicy } from './Policy';\n\ninterface IQueueItem {\n\tsignal: AbortSignal;\n\tfn(context: IDefaultPolicyContext): Promise | T;\n\tresolve(value: T): void;\n\treject(error: Error): void;\n}\n\nexport class BulkheadPolicy implements IPolicy {\n\tpublic declare readonly _altReturn: never;\n\n\tprivate active = 0;\n\tprivate readonly queue: Array> = [];\n\tprivate readonly onRejectEmitter = new EventEmitter();\n\tprivate readonly executor = new ExecuteWrapper();\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onSuccess = this.executor.onSuccess;\n\n\t/**\n\t * @inheritdoc\n\t */\n\tpublic readonly onFailure = this.executor.onFailure;\n\n\t/**\n\t * Emitter that fires when an item is rejected from the bulkhead.\n\t */\n\tpublic readonly onReject = this.onRejectEmitter.addListener;\n\n\t/**\n\t * Returns the number of available execution slots at this point in time.\n\t */\n\tpublic get executionSlots() {\n\t\treturn this.capacity - this.active;\n\t}\n\n\t/**\n\t * Returns the number of queue slots at this point in time.\n\t */\n\tpublic get queueSlots() {\n\t\treturn this.queueCapacity - this.queue.length;\n\t}\n\n\t/**\n\t * Bulkhead limits concurrent requests made.\n\t */\n\tconstructor(private readonly capacity: number, private readonly queueCapacity: number) { }\n\n\t/**\n\t * Executes the given function.\n\t * @param fn Function to execute\n\t * @throws a {@link BulkheadRejectedException} if the bulkhead limits are exceeeded\n\t */\n\tpublic async execute(\n\t\tfn: (context: IDefaultPolicyContext) => PromiseLike | T,\n\t\tsignal = neverAbortedSignal,\n\t): Promise {\n\t\tif (signal.aborted) {\n\t\t\tthrow new TaskCancelledError();\n\t\t}\n\n\t\tif (this.active < this.capacity) {\n\t\t\tthis.active++;\n\t\t\ttry {\n\t\t\t\treturn await fn({ signal });\n\t\t\t} finally {\n\t\t\t\tthis.active--;\n\t\t\t\tthis.dequeue();\n\t\t\t}\n\t\t}\n\n\t\tif (this.queue.length >= this.queueCapacity) {\n\t\t\tthis.onRejectEmitter.emit();\n\t\t\tthrow new BulkheadRejectedError(this.capacity, this.queueCapacity);\n\t\t}\n\t\tconst { resolve, reject, promise } = defer();\n\t\tthis.queue.push({ signal, fn, resolve, reject });\n\t\treturn promise;\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[81,103)", + "modifiedRange": "[81,88)", + "innerChanges": [ + { + "originalRange": "[81,26 -> 81,26]", + "modifiedRange": "[81,26 -> 81,27]" + }, + { + "originalRange": "[81,48 -> 86,1 EOL]", + "modifiedRange": "[81,49 -> 81,49 EOL]" + }, + { + "originalRange": "[87,1 -> 87,1]", + "modifiedRange": "[82,1 -> 82,2]" + }, + { + "originalRange": "[88,1 -> 88,1]", + "modifiedRange": "[83,1 -> 83,2]" + }, + { + "originalRange": "[89,1 -> 89,1]", + "modifiedRange": "[84,1 -> 84,2]" + }, + { + "originalRange": "[90,1 -> 92,1]", + "modifiedRange": "[85,1 -> 85,1]" + }, + { + "originalRange": "[92,9 -> 97,4]", + "modifiedRange": "[85,9 -> 85,29]" + }, + { + "originalRange": "[97,10 -> 97,20 EOL]", + "modifiedRange": "[85,35 -> 85,51 EOL]" + }, + { + "originalRange": "[98,3 -> 98,16]", + "modifiedRange": "[86,3 -> 86,3]" + }, + { + "originalRange": "[98,21 -> 98,43]", + "modifiedRange": "[86,8 -> 86,21]" + }, + { + "originalRange": "[98,49 -> 99,15]", + "modifiedRange": "[86,27 -> 86,33]" + }, + { + "originalRange": "[99,22 -> 100,16]", + "modifiedRange": "[86,40 -> 86,42]" + }, + { + "originalRange": "[100,22 -> 100,22]", + "modifiedRange": "[86,48 -> 86,50]" + }, + { + "originalRange": "[101,2 -> 102,2 EOL]", + "modifiedRange": "[87,2 -> 87,18 EOL]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 7deb56947e1..584faab47ac 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3735,6 +3735,11 @@ declare namespace monaco.editor { * Defaults to false. */ peekWidgetDefaultFocus?: 'tree' | 'editor'; + /** + * Sets a placeholder for the editor. + * If set, the placeholder is shown if the editor is empty. + */ + placeholder?: string | undefined; /** * Controls whether the definition link opens element in the peek widget. * Defaults to false. @@ -4318,6 +4323,10 @@ declare namespace monaco.editor { * Font size of section headers. Defaults to 9. */ sectionHeaderFontSize?: number; + /** + * Spacing between the section header characters (in CSS px). Defaults to 1. + */ + sectionHeaderLetterSpacing?: number; } /** @@ -4562,7 +4571,6 @@ declare namespace monaco.editor { * Does not clear active inline suggestions when the editor loses focus. */ keepOnBlur?: boolean; - backgroundColoring?: boolean; } export interface IBracketPairColorizationOptions { @@ -4925,68 +4933,69 @@ declare namespace monaco.editor { pasteAs = 85, parameterHints = 86, peekWidgetDefaultFocus = 87, - definitionLinkOpensInPeek = 88, - quickSuggestions = 89, - quickSuggestionsDelay = 90, - readOnly = 91, - readOnlyMessage = 92, - renameOnType = 93, - renderControlCharacters = 94, - renderFinalNewline = 95, - renderLineHighlight = 96, - renderLineHighlightOnlyWhenFocus = 97, - renderValidationDecorations = 98, - renderWhitespace = 99, - revealHorizontalRightPadding = 100, - roundedSelection = 101, - rulers = 102, - scrollbar = 103, - scrollBeyondLastColumn = 104, - scrollBeyondLastLine = 105, - scrollPredominantAxis = 106, - selectionClipboard = 107, - selectionHighlight = 108, - selectOnLineNumbers = 109, - showFoldingControls = 110, - showUnused = 111, - snippetSuggestions = 112, - smartSelect = 113, - smoothScrolling = 114, - stickyScroll = 115, - stickyTabStops = 116, - stopRenderingLineAfter = 117, - suggest = 118, - suggestFontSize = 119, - suggestLineHeight = 120, - suggestOnTriggerCharacters = 121, - suggestSelection = 122, - tabCompletion = 123, - tabIndex = 124, - unicodeHighlighting = 125, - unusualLineTerminators = 126, - useShadowDOM = 127, - useTabStops = 128, - wordBreak = 129, - wordSegmenterLocales = 130, - wordSeparators = 131, - wordWrap = 132, - wordWrapBreakAfterCharacters = 133, - wordWrapBreakBeforeCharacters = 134, - wordWrapColumn = 135, - wordWrapOverride1 = 136, - wordWrapOverride2 = 137, - wrappingIndent = 138, - wrappingStrategy = 139, - showDeprecated = 140, - inlayHints = 141, - editorClassName = 142, - pixelRatio = 143, - tabFocusMode = 144, - layoutInfo = 145, - wrappingInfo = 146, - defaultColorDecorators = 147, - colorDecoratorsActivatedOn = 148, - inlineCompletionsAccessibilityVerbose = 149 + placeholder = 88, + definitionLinkOpensInPeek = 89, + quickSuggestions = 90, + quickSuggestionsDelay = 91, + readOnly = 92, + readOnlyMessage = 93, + renameOnType = 94, + renderControlCharacters = 95, + renderFinalNewline = 96, + renderLineHighlight = 97, + renderLineHighlightOnlyWhenFocus = 98, + renderValidationDecorations = 99, + renderWhitespace = 100, + revealHorizontalRightPadding = 101, + roundedSelection = 102, + rulers = 103, + scrollbar = 104, + scrollBeyondLastColumn = 105, + scrollBeyondLastLine = 106, + scrollPredominantAxis = 107, + selectionClipboard = 108, + selectionHighlight = 109, + selectOnLineNumbers = 110, + showFoldingControls = 111, + showUnused = 112, + snippetSuggestions = 113, + smartSelect = 114, + smoothScrolling = 115, + stickyScroll = 116, + stickyTabStops = 117, + stopRenderingLineAfter = 118, + suggest = 119, + suggestFontSize = 120, + suggestLineHeight = 121, + suggestOnTriggerCharacters = 122, + suggestSelection = 123, + tabCompletion = 124, + tabIndex = 125, + unicodeHighlighting = 126, + unusualLineTerminators = 127, + useShadowDOM = 128, + useTabStops = 129, + wordBreak = 130, + wordSegmenterLocales = 131, + wordSeparators = 132, + wordWrap = 133, + wordWrapBreakAfterCharacters = 134, + wordWrapBreakBeforeCharacters = 135, + wordWrapColumn = 136, + wordWrapOverride1 = 137, + wordWrapOverride2 = 138, + wrappingIndent = 139, + wrappingStrategy = 140, + showDeprecated = 141, + inlayHints = 142, + editorClassName = 143, + pixelRatio = 144, + tabFocusMode = 145, + layoutInfo = 146, + wrappingInfo = 147, + defaultColorDecorators = 148, + colorDecoratorsActivatedOn = 149, + inlineCompletionsAccessibilityVerbose = 150 } export const EditorOptions: { @@ -4999,8 +5008,8 @@ declare namespace monaco.editor { screenReaderAnnounceInlineSuggestion: IEditorOption; autoClosingBrackets: IEditorOption; autoClosingComments: IEditorOption; - autoClosingDelete: IEditorOption; - autoClosingOvertype: IEditorOption; + autoClosingDelete: IEditorOption; + autoClosingOvertype: IEditorOption; autoClosingQuotes: IEditorOption; autoIndent: IEditorOption; automaticLayout: IEditorOption; @@ -5012,7 +5021,7 @@ declare namespace monaco.editor { codeLensFontFamily: IEditorOption; codeLensFontSize: IEditorOption; colorDecorators: IEditorOption; - colorDecoratorActivatedOn: IEditorOption; + colorDecoratorActivatedOn: IEditorOption; colorDecoratorsLimit: IEditorOption; columnSelection: IEditorOption; comments: IEditorOption>>; @@ -5079,6 +5088,7 @@ declare namespace monaco.editor { pasteAs: IEditorOption>>; parameterHints: IEditorOption>>; peekWidgetDefaultFocus: IEditorOption; + placeholder: IEditorOption; definitionLinkOpensInPeek: IEditorOption; quickSuggestions: IEditorOption; quickSuggestionsDelay: IEditorOption; @@ -5120,13 +5130,13 @@ declare namespace monaco.editor { tabCompletion: IEditorOption; tabIndex: IEditorOption; unicodeHighlight: IEditorOption; - unusualLineTerminators: IEditorOption; + unusualLineTerminators: IEditorOption; useShadowDOM: IEditorOption; useTabStops: IEditorOption; wordBreak: IEditorOption; wordSegmenterLocales: IEditorOption; wordSeparators: IEditorOption; - wordWrap: IEditorOption; + wordWrap: IEditorOption; wordWrapBreakAfterCharacters: IEditorOption; wordWrapBreakBeforeCharacters: IEditorOption; wordWrapColumn: IEditorOption; @@ -7935,6 +7945,11 @@ declare namespace monaco.languages { arguments?: any[]; } + export interface CommentThreadRevealOptions { + preserveFocus: boolean; + focusReply: boolean; + } + export interface CommentAuthorInformation { name: string; iconPath?: UriComponents; @@ -8049,7 +8064,7 @@ declare namespace monaco.languages { * * @param document The document to provide mapped edits for. * @param codeBlocks Code blocks that come from an LLM's reply. - * "Insert at cursor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. + * "Apply in Editor" in the panel chat only sends one edit that the user clicks on, but inline chat can send multiple blocks and let the lang server decide what to do with them. * @param context The context for providing mapped edits. * @param token A cancellation token. * @returns A provider result of text edits. diff --git a/src/vs/nls.build.ts b/src/vs/nls.build.ts deleted file mode 100644 index 05c063fa291..00000000000 --- a/src/vs/nls.build.ts +++ /dev/null @@ -1,88 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -const buildMap: { [name: string]: string[] } = {}; -const buildMapKeys: { [name: string]: string[] } = {}; -const entryPoints: { [entryPoint: string]: string[] } = {}; - -export interface ILocalizeInfo { - key: string; - comment: string[]; -} - -export function localize(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): string { - throw new Error(`Not supported at build time!`); -} - -export function localize2(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): never { - throw new Error(`Not supported at build time!`); -} - -export function getConfiguredDefaultLocale(): string | undefined { - throw new Error(`Not supported at build time!`); -} - -/** - * Invoked by the loader at build-time - */ -export function load(name: string, req: AMDLoader.IRelativeRequire, load: AMDLoader.IPluginLoadCallback, config: AMDLoader.IConfigurationOptions): void { - if (!name || name.length === 0) { - load({ localize, localize2, getConfiguredDefaultLocale }); - } else { - req([name + '.nls', name + '.nls.keys'], function (messages: string[], keys: string[]) { - buildMap[name] = messages; - buildMapKeys[name] = keys; - load(messages); - }); - } -} - -/** - * Invoked by the loader at build-time - */ -export function write(pluginName: string, moduleName: string, write: AMDLoader.IPluginWriteCallback): void { - const entryPoint = write.getEntryPoint(); - - entryPoints[entryPoint] = entryPoints[entryPoint] || []; - entryPoints[entryPoint].push(moduleName); - - if (moduleName !== entryPoint) { - write.asModule(pluginName + '!' + moduleName, 'define([\'vs/nls\', \'vs/nls!' + entryPoint + '\'], function(nls, data) { return nls.create("' + moduleName + '", data); });'); - } -} - -/** - * Invoked by the loader at build-time - */ -export function writeFile(pluginName: string, moduleName: string, req: AMDLoader.IRelativeRequire, write: AMDLoader.IPluginWriteFileCallback, config: AMDLoader.IConfigurationOptions): void { - if (entryPoints.hasOwnProperty(moduleName)) { - const fileName = req.toUrl(moduleName + '.nls.js'); - const contents = [ - '/*---------------------------------------------------------', - ' * Copyright (c) Microsoft Corporation. All rights reserved.', - ' *--------------------------------------------------------*/' - ], - entries = entryPoints[moduleName]; - - const data: { [moduleName: string]: string[] } = {}; - for (let i = 0; i < entries.length; i++) { - data[entries[i]] = buildMap[entries[i]]; - } - - contents.push('define("' + moduleName + '.nls", ' + JSON.stringify(data, null, '\t') + ');'); - write(fileName, contents.join('\r\n')); - } -} - -/** - * Invoked by the loader at build-time - */ -export function finishBuild(write: AMDLoader.IPluginWriteFileCallback): void { - write('nls.metadata.json', JSON.stringify({ - keys: buildMapKeys, - messages: buildMap, - bundles: entryPoints - }, null, '\t')); -} diff --git a/src/vs/nls.mock.ts b/src/vs/nls.mock.ts deleted file mode 100644 index 5323c6c6340..00000000000 --- a/src/vs/nls.mock.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export interface ILocalizeInfo { - key: string; - comment: string[]; -} - -export interface ILocalizedString { - original: string; - value: string; -} - -function _format(message: string, args: any[]): string { - let result: string; - if (args.length === 0) { - result = message; - } else { - result = message.replace(/\{(\d+)\}/g, function (match, rest) { - const index = rest[0]; - return typeof args[index] !== 'undefined' ? args[index] : match; - }); - } - return result; -} - -export function localize(data: ILocalizeInfo | string, message: string, ...args: any[]): string { - return _format(message, args); -} - -export function localize2(data: ILocalizeInfo | string, message: string, ...args: any[]): ILocalizedString { - const res = _format(message, args); - return { - original: res, - value: res - }; -} - -export function getConfiguredDefaultLocale(_: string) { - return undefined; -} diff --git a/src/vs/nls.ts b/src/vs/nls.ts index 233840e65ab..5a546325fc7 100644 --- a/src/vs/nls.ts +++ b/src/vs/nls.ts @@ -3,27 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -let isPseudo = (typeof document !== 'undefined' && document.location && document.location.hash.indexOf('pseudo=true') >= 0); -const DEFAULT_TAG = 'i-default'; - -interface INLSPluginConfig { - availableLanguages?: INLSPluginConfigAvailableLanguages; - loadBundle?: BundleLoader; - translationServiceUrl?: string; -} - -export interface INLSPluginConfigAvailableLanguages { - '*'?: string; - [module: string]: string | undefined; -} - -interface BundleLoader { - (bundle: string, locale: string | null, cb: (err: Error, messages: string[] | IBundledStrings) => void): void; -} - -interface IBundledStrings { - [moduleId: string]: string[]; -} +// VSCODE_GLOBALS: NLS +const isPseudo = globalThis._VSCODE_NLS_LANGUAGE === 'pseudo' || (typeof document !== 'undefined' && document.location && document.location.hash.indexOf('pseudo=true') >= 0); export interface ILocalizeInfo { key: string; @@ -35,30 +16,6 @@ export interface ILocalizedString { value: string; } -interface ILocalizeFunc { - (info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string; - (key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string; -} - -interface IBoundLocalizeFunc { - (idx: number, defaultValue: null): string; -} - -interface ILocalize2Func { - (info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString; - (key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString; -} - -interface IBoundLocalize2Func { - (idx: number, defaultValue: string): ILocalizedString; -} - -interface IConsumerAPI { - localize: ILocalizeFunc | IBoundLocalizeFunc; - localize2: ILocalize2Func | IBoundLocalize2Func; - getConfiguredDefaultLocale(stringFromLocalizeCall: string): string | undefined; -} - function _format(message: string, args: (string | number | boolean | undefined | null)[]): string { let result: string; @@ -86,49 +43,6 @@ function _format(message: string, args: (string | number | boolean | undefined | return result; } -function findLanguageForModule(config: INLSPluginConfigAvailableLanguages, name: string) { - let result = config[name]; - if (result) { - return result; - } - result = config['*']; - if (result) { - return result; - } - return null; -} - -function endWithSlash(path: string): string { - if (path.charAt(path.length - 1) === '/') { - return path; - } - return path + '/'; -} - -async function getMessagesFromTranslationsService(translationServiceUrl: string, language: string, name: string): Promise { - const url = endWithSlash(translationServiceUrl) + endWithSlash(language) + 'vscode/' + endWithSlash(name); - const res = await fetch(url); - if (res.ok) { - const messages = await res.json() as string[] | IBundledStrings; - return messages; - } - throw new Error(`${res.status} - ${res.statusText}`); -} - -function createScopedLocalize(scope: string[]): IBoundLocalizeFunc { - return function (idx: number, defaultValue: null) { - const restArgs = Array.prototype.slice.call(arguments, 2); - return _format(scope[idx], restArgs); - }; -} - -function createScopedLocalize2(scope: string[]): IBoundLocalize2Func { - return (idx: number, defaultValue: string, ...args) => ({ - value: _format(scope[idx], args), - original: _format(defaultValue, args) - }); -} - /** * Marks a string to be localized. Returns the localized string. * @@ -160,10 +74,30 @@ export function localize(key: string, message: string, ...args: (string | number /** * @skipMangle */ -export function localize(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): string { +export function localize(data: ILocalizeInfo | string /* | number when built */, message: string /* | null when built */, ...args: (string | number | boolean | undefined | null)[]): string { + if (typeof data === 'number') { + return _format(lookupMessage(data, message), args); + } return _format(message, args); } +/** + * Only used when built: Looks up the message in the global NLS table. + * This table is being made available as a global through bootstrapping + * depending on the target context. + */ +function lookupMessage(index: number, fallback: string | null): string { + // VSCODE_GLOBALS: NLS + const message = globalThis._VSCODE_NLS_MESSAGES?.[index]; + if (typeof message !== 'string') { + if (typeof fallback === 'string') { + return fallback; + } + throw new Error(`!!! NLS MISSING: ${index} !!!`); + } + return message; +} + /** * Marks a string to be localized. Returns an {@linkcode ILocalizedString} * which contains the localized string and the original string. @@ -197,123 +131,107 @@ export function localize2(key: string, message: string, ...args: (string | numbe /** * @skipMangle */ -export function localize2(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString { - const original = _format(message, args); - return { - value: original, - original - }; -} - -/** - * - * @param stringFromLocalizeCall You must pass in a string that was returned from a `nls.localize()` call - * in order to ensure the loader plugin has been initialized before this function is called. - */ -export function getConfiguredDefaultLocale(stringFromLocalizeCall: string): string | undefined; -/** - * @skipMangle - */ -export function getConfiguredDefaultLocale(_: string): string | undefined { - // This returns undefined because this implementation isn't used and is overwritten by the loader - // when loaded. - return undefined; -} - -/** - * @skipMangle - */ -export function setPseudoTranslation(value: boolean) { - isPseudo = value; -} - -/** - * Invoked in a built product at run-time - * @skipMangle - */ -export function create(key: string, data: IBundledStrings & IConsumerAPI): IConsumerAPI { - return { - localize: createScopedLocalize(data[key]), - localize2: createScopedLocalize2(data[key]), - getConfiguredDefaultLocale: data.getConfiguredDefaultLocale ?? ((_: string) => undefined) - }; -} - -/** - * Invoked by the loader at run-time - * @skipMangle - */ -export function load(name: string, req: AMDLoader.IRelativeRequire, load: AMDLoader.IPluginLoadCallback, config: AMDLoader.IConfigurationOptions): void { - const pluginConfig: INLSPluginConfig = config['vs/nls'] ?? {}; - if (!name || name.length === 0) { - // TODO: We need to give back the mangled names here - return load({ - localize: localize, - localize2: localize2, - getConfiguredDefaultLocale: () => pluginConfig.availableLanguages?.['*'] - } as IConsumerAPI); - } - const language = pluginConfig.availableLanguages ? findLanguageForModule(pluginConfig.availableLanguages, name) : null; - const useDefaultLanguage = language === null || language === DEFAULT_TAG; - let suffix = '.nls'; - if (!useDefaultLanguage) { - suffix = suffix + '.' + language; - } - const messagesLoaded = (messages: string[] | IBundledStrings) => { - if (Array.isArray(messages)) { - (messages as any as IConsumerAPI).localize = createScopedLocalize(messages); - (messages as any as IConsumerAPI).localize2 = createScopedLocalize2(messages); - } else { - (messages as any as IConsumerAPI).localize = createScopedLocalize(messages[name]); - (messages as any as IConsumerAPI).localize2 = createScopedLocalize2(messages[name]); - } - (messages as any as IConsumerAPI).getConfiguredDefaultLocale = () => pluginConfig.availableLanguages?.['*']; - load(messages); - }; - if (typeof pluginConfig.loadBundle === 'function') { - (pluginConfig.loadBundle as BundleLoader)(name, language, (err: Error, messages) => { - // We have an error. Load the English default strings to not fail - if (err) { - req([name + '.nls'], messagesLoaded); - } else { - messagesLoaded(messages); - } - }); - } else if (pluginConfig.translationServiceUrl && !useDefaultLanguage) { - (async () => { - try { - const messages = await getMessagesFromTranslationsService(pluginConfig.translationServiceUrl!, language, name); - return messagesLoaded(messages); - } catch (err) { - // Language is already as generic as it gets, so require default messages - if (!language.includes('-')) { - console.error(err); - return req([name + '.nls'], messagesLoaded); - } - try { - // Since there is a dash, the language configured is a specific sub-language of the same generic language. - // Since we were unable to load the specific language, try to load the generic language. Ex. we failed to find a - // Swiss German (de-CH), so try to load the generic German (de) messages instead. - const genericLanguage = language.split('-')[0]; - const messages = await getMessagesFromTranslationsService(pluginConfig.translationServiceUrl!, genericLanguage, name); - // We got some messages, so we configure the configuration to use the generic language for this session. - pluginConfig.availableLanguages ??= {}; - pluginConfig.availableLanguages['*'] = genericLanguage; - return messagesLoaded(messages); - } catch (err) { - console.error(err); - return req([name + '.nls'], messagesLoaded); - } - } - })(); +export function localize2(data: ILocalizeInfo | string /* | number when built */, originalMessage: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString { + let message: string; + if (typeof data === 'number') { + message = lookupMessage(data, originalMessage); } else { - req([name + suffix], messagesLoaded, (err: Error) => { - if (suffix === '.nls') { - console.error('Failed trying to load default language strings', err); - return; - } - console.error(`Failed to load message bundle for language ${language}. Falling back to the default language:`, err); - req([name + '.nls'], messagesLoaded); - }); + message = originalMessage; } + + const value = _format(message, args); + + return { + value, + original: originalMessage === message ? value : _format(originalMessage, args) + }; } + +export interface INLSLanguagePackConfiguration { + + /** + * The path to the translations config file that contains pointers to + * all message bundles for `main` and extensions. + */ + readonly translationsConfigFile: string; + + /** + * The path to the file containing the translations for this language + * pack as flat string array. + */ + readonly messagesFile: string; + + /** + * The path to the file that can be used to signal a corrupt language + * pack, for example when reading the `messagesFile` fails. This will + * instruct the application to re-create the cache on next startup. + */ + readonly corruptMarkerFile: string; +} + +export interface INLSConfiguration { + + /** + * Locale as defined in `argv.json` or `app.getLocale()`. + */ + readonly userLocale: string; + + /** + * Locale as defined by the OS (e.g. `app.getPreferredSystemLanguages()`). + */ + readonly osLocale: string; + + /** + * The actual language of the UI that ends up being used considering `userLocale` + * and `osLocale`. + */ + readonly resolvedLanguage: string; + + /** + * Defined if a language pack is used that is not the + * default english language pack. This requires a language + * pack to be installed as extension. + */ + readonly languagePack?: INLSLanguagePackConfiguration; + + /** + * The path to the file containing the default english messages + * as flat string array. The file is only present in built + * versions of the application. + */ + readonly defaultMessagesFile: string; + + /** + * Below properties are deprecated and only there to continue support + * for `vscode-nls` module that depends on them. + * Refs https://github.com/microsoft/vscode-nls/blob/main/src/node/main.ts#L36-L46 + */ + /** @deprecated */ + readonly locale: string; + /** @deprecated */ + readonly availableLanguages: Record; + /** @deprecated */ + readonly _languagePackSupport?: boolean; + /** @deprecated */ + readonly _languagePackId?: string; + /** @deprecated */ + readonly _translationsConfigFile?: string; + /** @deprecated */ + readonly _cacheRoot?: string; + /** @deprecated */ + readonly _resolvedLanguagePackCoreLocation?: string; + /** @deprecated */ + readonly _corruptedFile?: string; +} + +export interface ILanguagePack { + readonly hash: string; + readonly label: string | undefined; + readonly extensions: { + readonly extensionIdentifier: { readonly id: string; readonly uuid?: string }; + readonly version: string; + }[]; + readonly translations: Record; +} + +export type ILanguagePacks = Record; diff --git a/src/vs/platform/accessibility/browser/accessibilityService.ts b/src/vs/platform/accessibility/browser/accessibilityService.ts index bd84abbc6dc..408fbc07b2a 100644 --- a/src/vs/platform/accessibility/browser/accessibilityService.ts +++ b/src/vs/platform/accessibility/browser/accessibilityService.ts @@ -24,6 +24,9 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe protected _systemMotionReduced: boolean; protected readonly _onDidChangeReducedMotion = new Emitter(); + private _linkUnderlinesEnabled: boolean; + protected readonly _onDidChangeLinkUnderline = new Emitter(); + constructor( @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ILayoutService private readonly _layoutService: ILayoutService, @@ -50,7 +53,10 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe this._systemMotionReduced = reduceMotionMatcher.matches; this._configMotionReduced = this._configurationService.getValue<'auto' | 'on' | 'off'>('workbench.reduceMotion'); + this._linkUnderlinesEnabled = this._configurationService.getValue('accessibility.underlineLinks'); + this.initReducedMotionListeners(reduceMotionMatcher); + this.initLinkUnderlineListeners(); } private initReducedMotionListeners(reduceMotionMatcher: MediaQueryList) { @@ -72,6 +78,29 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe this._register(this.onDidChangeReducedMotion(() => updateRootClasses())); } + private initLinkUnderlineListeners() { + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('accessibility.underlineLinks')) { + const linkUnderlinesEnabled = this._configurationService.getValue('accessibility.underlineLinks'); + this._linkUnderlinesEnabled = linkUnderlinesEnabled; + this._onDidChangeLinkUnderline.fire(); + } + })); + + const updateLinkUnderlineClasses = () => { + const underlineLinks = this._linkUnderlinesEnabled; + this._layoutService.mainContainer.classList.toggle('underline-links', underlineLinks); + }; + + updateLinkUnderlineClasses(); + + this._register(this.onDidChangeLinkUnderlines(() => updateLinkUnderlineClasses())); + } + + public onDidChangeLinkUnderlines(listener: () => void) { + return this._onDidChangeLinkUnderline.event(listener); + } + get onDidChangeScreenReaderOptimized(): Event { return this._onDidChangeScreenReaderOptimized.event; } diff --git a/src/vs/platform/accessibility/browser/accessibleView.ts b/src/vs/platform/accessibility/browser/accessibleView.ts index d2fedcb8661..d71a5871195 100644 --- a/src/vs/platform/accessibility/browser/accessibleView.ts +++ b/src/vs/platform/accessibility/browser/accessibleView.ts @@ -141,9 +141,11 @@ export class AdvancedContentProvider implements IAccessibleViewContentProvider { public provideContent: () => string, public onClose: () => void, public verbositySettingKey: string, + public onOpen?: () => void, public actions?: IAction[], public next?: () => void, public previous?: () => void, + public onDidChangeContent?: Event, public onKeyDown?: (e: IKeyboardEvent) => void, public getSymbols?: () => IAccessibleViewSymbol[], public onDidRequestClearLastProvider?: Event, @@ -157,9 +159,11 @@ export class ExtensionContentProvider implements IBasicContentProvider { public options: IAccessibleViewOptions, public provideContent: () => string, public onClose: () => void, + public onOpen?: () => void, public next?: () => void, public previous?: () => void, public actions?: IAction[], + public onDidChangeContent?: Event, ) { } } @@ -168,7 +172,9 @@ export interface IBasicContentProvider { options: IAccessibleViewOptions; onClose(): void; provideContent(): string; + onOpen?(): void; actions?: IAction[]; previous?(): void; next?(): void; + onDidChangeContent?: Event; } diff --git a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts index 13679e64781..0a6369565bb 100644 --- a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts +++ b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts @@ -9,7 +9,7 @@ import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { alert } from 'vs/base/browser/ui/aria/aria'; -export interface IAccessibleViewImplentation { +export interface IAccessibleViewImplentation extends IDisposable { type: AccessibleViewType; priority: number; name: string; @@ -31,6 +31,7 @@ export const AccessibleViewRegistry = new class AccessibleViewRegistry { if (idx !== -1) { this._implementations.splice(idx, 1); } + implementation.dispose(); } }; } diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 65515c237ad..51d518e00f8 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -26,7 +26,7 @@ export interface IAccessibilitySignalService { playSignalLoop(signal: AccessibilitySignal, milliseconds: number): IDisposable; getEnabledState(signal: AccessibilitySignal, userGesture: boolean, modality?: AccessibilityModality | undefined): IValueWithChangeEvent; - getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality): number; + getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality, mode: 'line' | 'positional'): number; /** * Avoid this method and prefer `.playSignal`! * Only use it when you want to play the sound regardless of enablement, e.g. in the settings quick pick. @@ -67,7 +67,7 @@ export interface IAccessbilitySignalOptions { export class AccessibilitySignalService extends Disposable implements IAccessibilitySignalService { readonly _serviceBrand: undefined; private readonly sounds: Map = new Map(); - private readonly screenReaderAttached = observableFromEvent( + private readonly screenReaderAttached = observableFromEvent(this, this.accessibilityService.onDidChangeScreenReaderOptimized, () => /** @description accessibilityService.onDidChangeScreenReaderOptimized */ this.accessibilityService.isScreenReaderOptimized() ); @@ -241,10 +241,19 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi return this.getEnabledState(signal, false).onDidChange; } - public getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality): number { - const delaySettingsKey = signal.delaySettingsKey ?? 'accessibility.signalOptions.delays.general'; - const delaySettingsValue: { sound: number; announcement: number } = this.configurationService.getValue(delaySettingsKey); - return modality === 'sound' ? delaySettingsValue.sound : delaySettingsValue.announcement; + public getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality, mode: 'line' | 'positional'): number { + if (!this.configurationService.getValue('accessibility.signalOptions.debouncePositionChanges')) { + return 0; + } + let value: { sound: number; announcement: number }; + if (signal.name === AccessibilitySignal.errorAtPosition.name && mode === 'positional') { + value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.errorAtPosition'); + } else if (signal.name === AccessibilitySignal.warningAtPosition.name && mode === 'positional') { + value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.warningAtPosition'); + } else { + value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.general'); + } + return modality === 'sound' ? value.sound : value.announcement; } } diff --git a/src/vs/platform/actionWidget/browser/actionWidget.ts b/src/vs/platform/actionWidget/browser/actionWidget.ts index e7b26382eae..20a030b67ab 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.ts +++ b/src/vs/platform/actionWidget/browser/actionWidget.ts @@ -21,7 +21,7 @@ import { inputActiveOptionBackground, registerColor } from 'vs/platform/theme/co registerColor( 'actionBar.toggledBackground', - { dark: inputActiveOptionBackground, light: inputActiveOptionBackground, hcDark: inputActiveOptionBackground, hcLight: inputActiveOptionBackground, }, + inputActiveOptionBackground, localize('actionBar.toggledBackground', 'Background color for toggled action items in action bar.') ); diff --git a/src/vs/platform/actions/browser/buttonbar.ts b/src/vs/platform/actions/browser/buttonbar.ts index 165caec0bf1..530b1fe6170 100644 --- a/src/vs/platform/actions/browser/buttonbar.ts +++ b/src/vs/platform/actions/browser/buttonbar.ts @@ -6,11 +6,14 @@ import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ActionRunner, IAction, IActionRunner, SubmenuAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; -import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IToolBarRenderOptions } from 'vs/platform/actions/browser/toolbar'; +import { MenuId, IMenuService, MenuItemAction, IMenuActionOptions } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IHoverService } from 'vs/platform/hover/browser/hover'; @@ -66,7 +69,7 @@ export class WorkbenchButtonBar extends ButtonBar { super.dispose(); } - update(actions: IAction[]): void { + update(actions: IAction[], secondary: IAction[]): void { const conifgProvider: IButtonConfigProvider = this._options?.buttonConfigProvider ?? (() => ({ showLabel: true })); @@ -122,21 +125,51 @@ export class WorkbenchButtonBar extends ButtonBar { } else { tooltip = action.label; } - this._updateStore.add(this._hoverService.setupUpdatableHover(hoverDelegate, btn.element, tooltip)); + this._updateStore.add(this._hoverService.setupManagedHover(hoverDelegate, btn.element, tooltip)); this._updateStore.add(btn.onDidClick(async () => { this._actionRunner.run(action); })); } + + if (secondary.length > 0) { + + const btn = this.addButton({ + secondary: true, + ariaLabel: localize('moreActions', "More Actions") + }); + + btn.icon = Codicon.dropDownButton; + btn.element.classList.add('default-colors', 'monaco-text-button'); + + btn.enabled = true; + this._updateStore.add(this._hoverService.setupManagedHover(hoverDelegate, btn.element, localize('moreActions', "More Actions"))); + this._updateStore.add(btn.onDidClick(async () => { + this._contextMenuService.showContextMenu({ + getAnchor: () => btn.element, + getActions: () => secondary, + actionRunner: this._actionRunner, + onHide: () => btn.element.setAttribute('aria-expanded', 'false') + }); + btn.element.setAttribute('aria-expanded', 'true'); + + })); + } this._onDidChange.fire(this); } } +export interface IMenuWorkbenchButtonBarOptions extends IWorkbenchButtonBarOptions { + menuOptions?: IMenuActionOptions; + + toolbarOptions?: IToolBarRenderOptions; +} + export class MenuWorkbenchButtonBar extends WorkbenchButtonBar { constructor( container: HTMLElement, menuId: MenuId, - options: IWorkbenchButtonBarOptions | undefined, + options: IMenuWorkbenchButtonBarOptions | undefined, @IMenuService menuService: IMenuService, @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, @@ -153,12 +186,16 @@ export class MenuWorkbenchButtonBar extends WorkbenchButtonBar { this.clear(); - const actions = menu - .getActions({ renderShortTitle: true }) - .flatMap(entry => entry[1]); - - super.update(actions); + const primary: IAction[] = []; + const secondary: IAction[] = []; + createAndFillInActionBarActions( + menu, + options?.menuOptions, + { primary, secondary }, + options?.toolbarOptions?.primaryGroup + ); + super.update(primary, secondary); }; this._store.add(menu.onDidChange(update)); update(); diff --git a/src/vs/platform/actions/browser/floatingMenu.ts b/src/vs/platform/actions/browser/floatingMenu.ts index e6840b10e10..e7285146aa0 100644 --- a/src/vs/platform/actions/browser/floatingMenu.ts +++ b/src/vs/platform/actions/browser/floatingMenu.ts @@ -119,7 +119,7 @@ export class FloatingClickMenu extends AbstractFloatingClickMenu { const w = this.instantiationService.createInstance(FloatingClickWidget, action.label); const node = w.getDomNode(); this.options.container.appendChild(node); - disposable.add(toDisposable(() => this.options.container.removeChild(node))); + disposable.add(toDisposable(() => node.remove())); return w; } diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.css b/src/vs/platform/actions/browser/menuEntryActionViewItem.css index c5cba140485..7eb35af7e4b 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.css +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.css @@ -11,6 +11,20 @@ background-size: 16px; } +.monaco-action-bar .action-item.menu-entry.text-only .action-label { + color: var(--vscode-descriptionForeground); + overflow: hidden; + border-radius: 2px; +} + +.monaco-action-bar .action-item.menu-entry.text-only.use-comma:not(:last-of-type) .action-label::after { + content: ', '; +} + +.monaco-action-bar .action-item.menu-entry.text-only + .action-item:not(.text-only) > .monaco-dropdown .action-label { + color: var(--vscode-descriptionForeground); +} + .monaco-dropdown-with-default { display: flex !important; flex-direction: row; diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index da596a8fc6c..58904af44aa 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -31,9 +31,25 @@ import { assertType } from 'vs/base/common/types'; import { asCssVariable, selectBorder } from 'vs/platform/theme/common/colorRegistry'; import { defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; + +export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string): void; +export function createAndFillInContextMenuActions(menu: [string, Array][], target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string): void; +export function createAndFillInContextMenuActions(menu: IMenu | [string, Array][], optionsOrTarget: IMenuActionOptions | undefined | IAction[] | { primary: IAction[]; secondary: IAction[] }, targetOrPrimaryGroup?: IAction[] | { primary: IAction[]; secondary: IAction[] } | string, primaryGroupOrUndefined?: string): void { + let target: IAction[] | { primary: IAction[]; secondary: IAction[] }; + let primaryGroup: string | ((actionGroup: string) => boolean) | undefined; + let groups: [string, Array][]; + if (Array.isArray(menu)) { + groups = menu; + target = optionsOrTarget as IAction[] | { primary: IAction[]; secondary: IAction[] }; + primaryGroup = targetOrPrimaryGroup as string | undefined; + } else { + const options: IMenuActionOptions | undefined = optionsOrTarget as IMenuActionOptions | undefined; + groups = menu.getActions(options); + target = targetOrPrimaryGroup as IAction[] | { primary: IAction[]; secondary: IAction[] }; + primaryGroup = primaryGroupOrUndefined; + } -export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string): void { - const groups = menu.getActions(options); const modifierKeyEmitter = ModifierKeyEmitter.getInstance(); const useAlternativeActions = modifierKeyEmitter.keyStatus.altKey || ((isWindows || isLinux) && modifierKeyEmitter.keyStatus.shiftKey); fillInActions(groups, target, useAlternativeActions, primaryGroup ? actionGroup => actionGroup === primaryGroup : actionGroup => actionGroup === 'navigation'); @@ -46,8 +62,42 @@ export function createAndFillInActionBarActions( primaryGroup?: string | ((actionGroup: string) => boolean), shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean, useSeparatorsInPrimaryActions?: boolean +): void; +export function createAndFillInActionBarActions( + menu: [string, Array][], + target: IAction[] | { primary: IAction[]; secondary: IAction[] }, + primaryGroup?: string | ((actionGroup: string) => boolean), + shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean, + useSeparatorsInPrimaryActions?: boolean +): void; +export function createAndFillInActionBarActions( + menu: IMenu | [string, Array][], + optionsOrTarget: IMenuActionOptions | undefined | IAction[] | { primary: IAction[]; secondary: IAction[] }, + targetOrPrimaryGroup?: IAction[] | { primary: IAction[]; secondary: IAction[] } | string | ((actionGroup: string) => boolean), + primaryGroupOrShouldInlineSubmenu?: string | ((actionGroup: string) => boolean) | ((action: SubmenuAction, group: string, groupSize: number) => boolean), + shouldInlineSubmenuOrUseSeparatorsInPrimaryActions?: ((action: SubmenuAction, group: string, groupSize: number) => boolean) | boolean, + useSeparatorsInPrimaryActionsOrUndefined?: boolean ): void { - const groups = menu.getActions(options); + let target: IAction[] | { primary: IAction[]; secondary: IAction[] }; + let primaryGroup: string | ((actionGroup: string) => boolean) | undefined; + let shouldInlineSubmenu: ((action: SubmenuAction, group: string, groupSize: number) => boolean) | undefined; + let useSeparatorsInPrimaryActions: boolean | undefined; + let groups: [string, Array][]; + if (Array.isArray(menu)) { + groups = menu; + target = optionsOrTarget as IAction[] | { primary: IAction[]; secondary: IAction[] }; + primaryGroup = targetOrPrimaryGroup as string | ((actionGroup: string) => boolean) | undefined; + shouldInlineSubmenu = primaryGroupOrShouldInlineSubmenu as (action: SubmenuAction, group: string, groupSize: number) => boolean; + useSeparatorsInPrimaryActions = shouldInlineSubmenuOrUseSeparatorsInPrimaryActions as boolean | undefined; + } else { + const options: IMenuActionOptions | undefined = optionsOrTarget as IMenuActionOptions | undefined; + groups = menu.getActions(options); + target = targetOrPrimaryGroup as IAction[] | { primary: IAction[]; secondary: IAction[] }; + primaryGroup = primaryGroupOrShouldInlineSubmenu as string | ((actionGroup: string) => boolean) | undefined; + shouldInlineSubmenu = shouldInlineSubmenuOrUseSeparatorsInPrimaryActions as (action: SubmenuAction, group: string, groupSize: number) => boolean; + useSeparatorsInPrimaryActions = useSeparatorsInPrimaryActionsOrUndefined; + } + const isPrimaryAction = typeof primaryGroup === 'string' ? (actionGroup: string) => actionGroup === primaryGroup : primaryGroup; // Action bars handle alternative actions on their own so the alternative actions should be ignored @@ -121,7 +171,7 @@ export interface IMenuEntryActionViewItemOptions { hoverDelegate?: IHoverDelegate; } -export class MenuEntryActionViewItem extends ActionViewItem { +export class MenuEntryActionViewItem extends ActionViewItem { private _wantsAltCommand: boolean = false; private readonly _itemClassDispose = this._register(new MutableDisposable()); @@ -129,7 +179,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { constructor( action: MenuItemAction, - options: IMenuEntryActionViewItemOptions | undefined, + protected _options: T | undefined, @IKeybindingService protected readonly _keybindingService: IKeybindingService, @INotificationService protected _notificationService: INotificationService, @IContextKeyService protected _contextKeyService: IContextKeyService, @@ -137,7 +187,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { @IContextMenuService protected _contextMenuService: IContextMenuService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService ) { - super(undefined, action, { icon: !!(action.class || action.item.icon), label: !action.class && !action.item.icon, draggable: options?.draggable, keybinding: options?.keybinding, hoverDelegate: options?.hoverDelegate }); + super(undefined, action, { icon: !!(action.class || action.item.icon), label: !action.class && !action.item.icon, draggable: _options?.draggable, keybinding: _options?.keybinding, hoverDelegate: _options?.hoverDelegate }); this._altKey = ModifierKeyEmitter.getInstance(); } @@ -285,6 +335,45 @@ export class MenuEntryActionViewItem extends ActionViewItem { } } +export interface ITextOnlyMenuEntryActionViewItemOptions extends IMenuEntryActionViewItemOptions { + conversational?: boolean; + useComma?: boolean; +} + +export class TextOnlyMenuEntryActionViewItem extends MenuEntryActionViewItem { + + override render(container: HTMLElement): void { + this.options.label = true; + this.options.icon = false; + super.render(container); + container.classList.add('text-only'); + container.classList.toggle('use-comma', this._options?.useComma ?? false); + } + + protected override updateLabel() { + const kb = this._keybindingService.lookupKeybinding(this._action.id, this._contextKeyService); + if (!kb) { + return super.updateLabel(); + } + if (this.label) { + const kb2 = TextOnlyMenuEntryActionViewItem._symbolPrintEnter(kb); + + if (this._options?.conversational) { + this.label.textContent = localize({ key: 'content2', comment: ['A label with keybindg like "ESC to dismiss"'] }, '{1} to {0}', this._action.label, kb2); + + } else { + this.label.textContent = localize({ key: 'content', comment: ['A label', 'A keybinding'] }, '{0} ({1})', this._action.label, kb2); + } + } + } + + private static _symbolPrintEnter(kb: ResolvedKeybinding) { + return kb.getLabel() + ?.replace(/\benter\b/gi, '\u23CE') + .replace(/\bEscape\b/gi, 'Esc'); + } +} + export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { constructor( diff --git a/src/vs/platform/actions/browser/toolbar.ts b/src/vs/platform/actions/browser/toolbar.ts index 4857d4bc07b..003ee6e781c 100644 --- a/src/vs/platform/actions/browser/toolbar.ts +++ b/src/vs/platform/actions/browser/toolbar.ts @@ -202,7 +202,9 @@ export class WorkbenchToolBar extends ToolBar { if (action instanceof MenuItemAction && action.menuKeybinding) { primaryActions.push(action.menuKeybinding); } else if (!(action instanceof SubmenuItemAction || action instanceof ToggleMenuAction)) { - primaryActions.push(createConfigureKeybindingAction(action.id, undefined, this._commandService, this._keybindingService)); + // only enable the configure keybinding action for actions that support keybindings + const supportsKeybindings = !!this._keybindingService.lookupKeybinding(action.id); + primaryActions.push(createConfigureKeybindingAction(this._commandService, this._keybindingService, action.id, undefined, supportsKeybindings)); } // -- Hide Actions -- diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index fa9bd76ed8a..8d087d8c9d9 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -138,6 +138,7 @@ export class MenuId { static readonly StickyScrollContext = new MenuId('StickyScrollContext'); static readonly TestItem = new MenuId('TestItem'); static readonly TestItemGutter = new MenuId('TestItemGutter'); + static readonly TestProfilesContext = new MenuId('TestProfilesContext'); static readonly TestMessageContext = new MenuId('TestMessageContext'); static readonly TestMessageContent = new MenuId('TestMessageContent'); static readonly TestPeekElement = new MenuId('TestPeekElement'); @@ -171,6 +172,8 @@ export class MenuId { static readonly InteractiveCellDelete = new MenuId('InteractiveCellDelete'); static readonly InteractiveCellExecute = new MenuId('InteractiveCellExecute'); static readonly InteractiveInputExecute = new MenuId('InteractiveInputExecute'); + static readonly InteractiveInputConfig = new MenuId('InteractiveInputConfig'); + static readonly ReplInputExecute = new MenuId('ReplInputExecute'); static readonly IssueReporter = new MenuId('IssueReporter'); static readonly NotebookToolbar = new MenuId('NotebookToolbar'); static readonly NotebookStickyScrollContext = new MenuId('NotebookStickyScrollContext'); @@ -209,6 +212,7 @@ export class MenuId { static readonly TerminalStickyScrollContext = new MenuId('TerminalStickyScrollContext'); static readonly WebviewContext = new MenuId('WebviewContext'); static readonly InlineCompletionsActions = new MenuId('InlineCompletionsActions'); + static readonly InlineEditsActions = new MenuId('InlineEditsActions'); static readonly InlineEditActions = new MenuId('InlineEditActions'); static readonly NewFile = new MenuId('NewFile'); static readonly MergeInput1Toolbar = new MenuId('MergeToolbar1Toolbar'); @@ -271,6 +275,11 @@ export interface IMenu extends IDisposable { getActions(options?: IMenuActionOptions): [string, Array][]; } +export interface IMenuData { + contexts: ReadonlySet; + actions: [string, Array][]; +} + export const IMenuService = createDecorator('menuService'); export interface IMenuCreateOptions { @@ -283,6 +292,8 @@ export interface IMenuService { readonly _serviceBrand: undefined; /** + * Consider using getMenuActions if you don't need to listen to events. + * * Create a new menu for the given menu identifier. A menu sends events when it's entries * have changed (placement, enablement, checked-state). By default it does not send events for * submenu entries. That is more expensive and must be explicitly enabled with the @@ -290,6 +301,16 @@ export interface IMenuService { */ createMenu(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuCreateOptions): IMenu; + /** + * Creates a new menu, gets the actions, and then disposes of the menu. + */ + getMenuActions(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuActionOptions): [string, Array][]; + + /** + * Gets the names of the contexts that this menu listens on. + */ + getMenuContexts(id: MenuId): ReadonlySet; + /** * Reset **all** menu item hidden states. */ diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 61fefa8e4ae..91d9ffdc49d 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -34,6 +34,18 @@ export class MenuService implements IMenuService { return new MenuImpl(id, this._hiddenStates, { emitEventsForSubmenuChanges: false, eventDebounceDelay: 50, ...options }, this._commandService, this._keybindingService, contextKeyService); } + getMenuActions(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuActionOptions): [string, Array][] { + const menu = new MenuImpl(id, this._hiddenStates, { emitEventsForSubmenuChanges: false, eventDebounceDelay: 50, ...options }, this._commandService, this._keybindingService, contextKeyService); + const actions = menu.getActions(options); + menu.dispose(); + return actions; + } + + getMenuContexts(id: MenuId): ReadonlySet { + const menuInfo = new MenuInfoSnapshot(id, false); + return new Set([...menuInfo.structureContextKeys, ...menuInfo.preconditionContextKeys, ...menuInfo.toggledContextKeys]); + } + resetHiddenStates(ids?: MenuId[]): void { this._hiddenStates.reset(ids); } @@ -152,20 +164,15 @@ class PersistedMenuHideState { type MenuItemGroup = [string, Array]; -class MenuInfo { - - private _menuGroups: MenuItemGroup[] = []; +class MenuInfoSnapshot { + protected _menuGroups: MenuItemGroup[] = []; private _structureContextKeys: Set = new Set(); private _preconditionContextKeys: Set = new Set(); private _toggledContextKeys: Set = new Set(); constructor( - private readonly _id: MenuId, - private readonly _hiddenStates: PersistedMenuHideState, - private readonly _collectContextKeysForSubmenus: boolean, - @ICommandService private readonly _commandService: ICommandService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService + protected readonly _id: MenuId, + protected readonly _collectContextKeysForSubmenus: boolean, ) { this.refresh(); } @@ -190,10 +197,8 @@ class MenuInfo { this._preconditionContextKeys.clear(); this._toggledContextKeys.clear(); - const menuItems = MenuRegistry.getMenuItems(this._id); - + const menuItems = this._sort(MenuRegistry.getMenuItems(this._id)); let group: MenuItemGroup | undefined; - menuItems.sort(MenuInfo._compareMenuItems); for (const item of menuItems) { // group by groupId @@ -209,19 +214,24 @@ class MenuInfo { } } + protected _sort(menuItems: (IMenuItem | ISubmenuItem)[]) { + // no sorting needed in snapshot + return menuItems; + } + private _collectContextKeys(item: IMenuItem | ISubmenuItem): void { - MenuInfo._fillInKbExprKeys(item.when, this._structureContextKeys); + MenuInfoSnapshot._fillInKbExprKeys(item.when, this._structureContextKeys); if (isIMenuItem(item)) { // keep precondition keys for event if applicable if (item.command.precondition) { - MenuInfo._fillInKbExprKeys(item.command.precondition, this._preconditionContextKeys); + MenuInfoSnapshot._fillInKbExprKeys(item.command.precondition, this._preconditionContextKeys); } // keep toggled keys for event if applicable if (item.command.toggled) { const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled; - MenuInfo._fillInKbExprKeys(toggledExpression, this._toggledContextKeys); + MenuInfoSnapshot._fillInKbExprKeys(toggledExpression, this._toggledContextKeys); } } else if (this._collectContextKeysForSubmenus) { @@ -231,6 +241,30 @@ class MenuInfo { } } + private static _fillInKbExprKeys(exp: ContextKeyExpression | undefined, set: Set): void { + if (exp) { + for (const key of exp.keys()) { + set.add(key); + } + } + } + +} + +class MenuInfo extends MenuInfoSnapshot { + + constructor( + _id: MenuId, + private readonly _hiddenStates: PersistedMenuHideState, + _collectContextKeysForSubmenus: boolean, + @ICommandService private readonly _commandService: ICommandService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService + ) { + super(_id, _collectContextKeysForSubmenus); + this.refresh(); + } + createActionGroups(options: IMenuActionOptions | undefined): [string, Array][] { const result: [string, Array][] = []; @@ -248,7 +282,7 @@ class MenuInfo { const menuHide = createMenuHide(this._id, isMenuItem ? item.command : item, this._hiddenStates); if (isMenuItem) { // MenuItemAction - const menuKeybinding = createConfigureKeybindingAction(item.command.id, item.when, this._commandService, this._keybindingService); + const menuKeybinding = createConfigureKeybindingAction(this._commandService, this._keybindingService, item.command.id, item.when); (activeActions ??= []).push(new MenuItemAction(item.command, item.alt, options, menuHide, menuKeybinding, this._contextKeyService, this._commandService)); } else { // SubmenuItemAction @@ -267,12 +301,8 @@ class MenuInfo { return result; } - private static _fillInKbExprKeys(exp: ContextKeyExpression | undefined, set: Set): void { - if (exp) { - for (const key of exp.keys()) { - set.add(key); - } - } + protected override _sort(menuItems: (IMenuItem | ISubmenuItem)[]): (IMenuItem | ISubmenuItem)[] { + return menuItems.sort(MenuInfo._compareMenuItems); } private static _compareMenuItems(a: IMenuItem | ISubmenuItem, b: IMenuItem | ISubmenuItem): number { @@ -442,10 +472,11 @@ function createMenuHide(menu: MenuId, command: ICommandAction | ISubmenuItem, st }; } -export function createConfigureKeybindingAction(commandId: string, when: ContextKeyExpression | undefined = undefined, commandService: ICommandService, keybindingService: IKeybindingService): IAction { +export function createConfigureKeybindingAction(commandService: ICommandService, keybindingService: IKeybindingService, commandId: string, when: ContextKeyExpression | undefined = undefined, enabled = true): IAction { return toAction({ id: `configureKeybinding/${commandId}`, label: localize('configure keybinding', "Configure Keybinding"), + enabled, run() { // Only set the when clause when there is no keybinding // It is possible that the action and the keybinding have different when clauses diff --git a/src/vs/platform/actions/test/common/menuService.test.ts b/src/vs/platform/actions/test/common/menuService.test.ts index 31e5d6c17be..8a3b2c421a9 100644 --- a/src/vs/platform/actions/test/common/menuService.test.ts +++ b/src/vs/platform/actions/test/common/menuService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { generateUuid } from 'vs/base/common/uuid'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 9fd5c045065..5ff0da22ae5 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { createHash } from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; @@ -131,7 +131,7 @@ flakySuite('BackupMainService', () => { environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS), { _serviceBrand: undefined, ...product }); - await Promises.mkdir(backupHome, { recursive: true }); + await fs.promises.mkdir(backupHome, { recursive: true }); configService = new TestConfigurationService(); stateMainService = new InMemoryTestStateMainService(); @@ -584,8 +584,8 @@ flakySuite('BackupMainService', () => { assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); try { - await Promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); - await Promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); + await fs.promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); + await fs.promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); } catch (error) { // ignore - folder might exist already } diff --git a/src/vs/platform/checksum/test/node/checksumService.test.ts b/src/vs/platform/checksum/test/node/checksumService.test.ts index 3e56af64720..5e7e71cdd7a 100644 --- a/src/vs/platform/checksum/test/node/checksumService.test.ts +++ b/src/vs/platform/checksum/test/node/checksumService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/clipboard/browser/clipboardService.ts b/src/vs/platform/clipboard/browser/clipboardService.ts index 7af565a3b34..d22b7bb5bc0 100644 --- a/src/vs/platform/clipboard/browser/clipboardService.ts +++ b/src/vs/platform/clipboard/browser/clipboardService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, isWebkitWebView } from 'vs/base/browser/browser'; -import { $, addDisposableListener, getActiveDocument, getActiveWindow, onDidRegisterWindow } from 'vs/base/browser/dom'; +import { $, addDisposableListener, getActiveDocument, getActiveWindow, isHTMLElement, onDidRegisterWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; import { DeferredPromise } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; @@ -130,11 +130,11 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer activeDocument.execCommand('copy'); - if (activeElement instanceof HTMLElement) { + if (isHTMLElement(activeElement)) { activeElement.focus(); } - activeDocument.body.removeChild(textArea); + textArea.remove(); } async readText(type?: string): Promise { diff --git a/src/vs/platform/commands/test/common/commands.test.ts b/src/vs/platform/commands/test/common/commands.test.ts index eeb3ed5da2a..f46f8e1a6ae 100644 --- a/src/vs/platform/commands/test/common/commands.test.ts +++ b/src/vs/platform/commands/test/common/commands.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { combinedDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 8b862b1c939..553c312d6be 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -279,11 +279,18 @@ export class ConfigurationModel implements IConfigurationModel { this.keys.push(key); } if (OVERRIDE_PROPERTY_REGEX.test(key)) { - this.overrides.push({ - identifiers: overrideIdentifiersFromKey(key), + const identifiers = overrideIdentifiersFromKey(key); + const override = { + identifiers, keys: Object.keys(this.contents[key]), contents: toValuesTree(this.contents[key], message => this.logService.error(message)), - }); + }; + const index = this.overrides.findIndex(o => arrays.equals(o.identifiers, identifiers)); + if (index !== -1) { + this.overrides[index] = override; + } else { + this.overrides.push(override); + } } } } diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index ed8e56d50c5..7876af5831e 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -70,10 +70,15 @@ export interface IConfigurationRegistry { */ deltaConfiguration(delta: IConfigurationDelta): void; + /** + * Return the registered default configurations + */ + getRegisteredDefaultConfigurations(): IConfigurationDefaults[]; + /** * Return the registered configuration defaults overrides */ - getConfigurationDefaultsOverrides(): Map; + getConfigurationDefaultsOverrides(): Map; /** * Signal that the schema of a configuration setting has changes. It is currently only supported to change enumeration values. @@ -191,6 +196,11 @@ export interface IConfigurationPropertySchema extends IJSONSchema { */ disallowSyncIgnore?: boolean; + /** + * Disallow extensions to contribute configuration default value for this setting. + */ + disallowConfigurationDefault?: boolean; + /** * Labels for enumeration items */ @@ -233,22 +243,28 @@ export interface IConfigurationNode { restrictedProperties?: string[]; } +export type ConfigurationDefaultValueSource = IExtensionInfo | Map; + export interface IConfigurationDefaults { overrides: IStringDictionary; - source?: IExtensionInfo | string; + source?: IExtensionInfo; } export type IRegisteredConfigurationPropertySchema = IConfigurationPropertySchema & { defaultDefaultValue?: any; source?: IExtensionInfo; // Source of the Property - defaultValueSource?: IExtensionInfo | string; // Source of the Default Value + defaultValueSource?: ConfigurationDefaultValueSource; // Source of the Default Value }; -export type IConfigurationDefaultOverride = { +export interface IConfigurationDefaultOverride { readonly value: any; - readonly source?: IExtensionInfo | string; // Source of the default override - readonly valuesSources?: Map; // Source of each value in default language overrides -}; + readonly source?: IExtensionInfo; // Source of the default override +} + +export interface IConfigurationDefaultOverrideValue { + readonly value: any; + readonly source?: ConfigurationDefaultValueSource; +} export const allSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; export const applicationSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; @@ -264,7 +280,8 @@ const contributionRegistry = Registry.as(JSONExtensio class ConfigurationRegistry implements IConfigurationRegistry { - private readonly configurationDefaultsOverrides: Map; + private readonly registeredConfigurationDefaults: IConfigurationDefaults[] = []; + private readonly configurationDefaultsOverrides: Map; private readonly defaultLanguageConfigurationOverridesNode: IConfigurationNode; private readonly configurationContributors: IConfigurationNode[]; private readonly configurationProperties: IStringDictionary; @@ -280,7 +297,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { readonly onDidUpdateConfiguration = this._onDidUpdateConfiguration.event; constructor() { - this.configurationDefaultsOverrides = new Map(); + this.configurationDefaultsOverrides = new Map(); this.defaultLanguageConfigurationOverridesNode = { id: 'defaultOverrides', title: nls.localize('defaultLanguageConfigurationOverrides.title', "Default Language Configuration Overrides"), @@ -343,43 +360,47 @@ class ConfigurationRegistry implements IConfigurationRegistry { private doRegisterDefaultConfigurations(configurationDefaults: IConfigurationDefaults[], bucket: Set) { + this.registeredConfigurationDefaults.push(...configurationDefaults); + const overrideIdentifiers: string[] = []; for (const { overrides, source } of configurationDefaults) { for (const key in overrides) { bucket.add(key); + const configurationDefaultOverridesForKey = this.configurationDefaultsOverrides.get(key) + ?? this.configurationDefaultsOverrides.set(key, { configurationDefaultOverrides: [] }).get(key)!; + + const value = overrides[key]; + configurationDefaultOverridesForKey.configurationDefaultOverrides.push({ value, source }); + + // Configuration defaults for Override Identifiers if (OVERRIDE_PROPERTY_REGEX.test(key)) { - const configurationDefaultOverride = this.configurationDefaultsOverrides.get(key); - const valuesSources = configurationDefaultOverride?.valuesSources ?? new Map(); - if (source) { - for (const configuration of Object.keys(overrides[key])) { - valuesSources.set(configuration, source); - } + const newDefaultOverride = this.mergeDefaultConfigurationsForOverrideIdentifier(key, value, source, configurationDefaultOverridesForKey.configurationDefaultOverrideValue); + if (!newDefaultOverride) { + continue; } - const defaultValue = { ...(configurationDefaultOverride?.value || {}), ...overrides[key] }; - this.configurationDefaultsOverrides.set(key, { source, value: defaultValue, valuesSources }); - const plainKey = getLanguageTagSettingPlainKey(key); - const property: IRegisteredConfigurationPropertySchema = { - type: 'object', - default: defaultValue, - description: nls.localize('defaultLanguageConfiguration.description', "Configure settings to be overridden for the {0} language.", plainKey), - $ref: resourceLanguageSettingsSchemaId, - defaultDefaultValue: defaultValue, - source: types.isString(source) ? undefined : source, - defaultValueSource: source - }; + + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = newDefaultOverride; + this.updateDefaultOverrideProperty(key, newDefaultOverride, source); overrideIdentifiers.push(...overrideIdentifiersFromKey(key)); - this.configurationProperties[key] = property; - this.defaultLanguageConfigurationOverridesNode.properties![key] = property; - } else { - this.configurationDefaultsOverrides.set(key, { value: overrides[key], source }); + } + + // Configuration defaults for Configuration Properties + else { + const newDefaultOverride = this.mergeDefaultConfigurationsForConfigurationProperty(key, value, source, configurationDefaultOverridesForKey.configurationDefaultOverrideValue); + if (!newDefaultOverride) { + continue; + } + + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = newDefaultOverride; const property = this.configurationProperties[key]; if (property) { this.updatePropertyDefaultValue(key, property); this.updateSchema(key, property); } } + } } @@ -394,31 +415,147 @@ class ConfigurationRegistry implements IConfigurationRegistry { } private doDeregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[], bucket: Set): void { + for (const defaultConfiguration of defaultConfigurations) { + const index = this.registeredConfigurationDefaults.indexOf(defaultConfiguration); + if (index !== -1) { + this.registeredConfigurationDefaults.splice(index, 1); + } + } for (const { overrides, source } of defaultConfigurations) { for (const key in overrides) { - const configurationDefaultsOverride = this.configurationDefaultsOverrides.get(key); - const id = types.isString(source) ? source : source?.id; - const configurationDefaultsOverrideSourceId = types.isString(configurationDefaultsOverride?.source) ? configurationDefaultsOverride?.source : configurationDefaultsOverride?.source?.id; - if (id !== configurationDefaultsOverrideSourceId) { + const configurationDefaultOverridesForKey = this.configurationDefaultsOverrides.get(key); + if (!configurationDefaultOverridesForKey) { continue; } - bucket.add(key); - this.configurationDefaultsOverrides.delete(key); + + const index = configurationDefaultOverridesForKey.configurationDefaultOverrides + .findIndex(configurationDefaultOverride => source ? configurationDefaultOverride.source?.id === source.id : configurationDefaultOverride.value === overrides[key]); + if (index === -1) { + continue; + } + + configurationDefaultOverridesForKey.configurationDefaultOverrides.splice(index, 1); + if (configurationDefaultOverridesForKey.configurationDefaultOverrides.length === 0) { + this.configurationDefaultsOverrides.delete(key); + } + if (OVERRIDE_PROPERTY_REGEX.test(key)) { - delete this.configurationProperties[key]; - delete this.defaultLanguageConfigurationOverridesNode.properties![key]; + let configurationDefaultOverrideValue: IConfigurationDefaultOverrideValue | undefined; + for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) { + configurationDefaultOverrideValue = this.mergeDefaultConfigurationsForOverrideIdentifier(key, configurationDefaultOverride.value, configurationDefaultOverride.source, configurationDefaultOverrideValue); + } + if (configurationDefaultOverrideValue && !types.isEmptyObject(configurationDefaultOverrideValue.value)) { + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = configurationDefaultOverrideValue; + this.updateDefaultOverrideProperty(key, configurationDefaultOverrideValue, source); + } else { + this.configurationDefaultsOverrides.delete(key); + delete this.configurationProperties[key]; + delete this.defaultLanguageConfigurationOverridesNode.properties![key]; + } } else { + let configurationDefaultOverrideValue: IConfigurationDefaultOverrideValue | undefined; + for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) { + configurationDefaultOverrideValue = this.mergeDefaultConfigurationsForConfigurationProperty(key, configurationDefaultOverride.value, configurationDefaultOverride.source, configurationDefaultOverrideValue); + } + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = configurationDefaultOverrideValue; const property = this.configurationProperties[key]; if (property) { this.updatePropertyDefaultValue(key, property); this.updateSchema(key, property); } } + bucket.add(key); + } + } + this.updateOverridePropertyPatternKey(); + } + + private updateDefaultOverrideProperty(key: string, newDefaultOverride: IConfigurationDefaultOverrideValue, source: IExtensionInfo | undefined): void { + const property: IRegisteredConfigurationPropertySchema = { + type: 'object', + default: newDefaultOverride.value, + description: nls.localize('defaultLanguageConfiguration.description', "Configure settings to be overridden for the {0} language.", getLanguageTagSettingPlainKey(key)), + $ref: resourceLanguageSettingsSchemaId, + defaultDefaultValue: newDefaultOverride.value, + source, + defaultValueSource: source + }; + this.configurationProperties[key] = property; + this.defaultLanguageConfigurationOverridesNode.properties![key] = property; + } + + private mergeDefaultConfigurationsForOverrideIdentifier(overrideIdentifier: string, configurationValueObject: IStringDictionary, valueSource: IExtensionInfo | undefined, existingDefaultOverride: IConfigurationDefaultOverrideValue | undefined): IConfigurationDefaultOverrideValue | undefined { + const defaultValue = existingDefaultOverride?.value || {}; + const source = existingDefaultOverride?.source ?? new Map(); + + // This should not happen + if (!(source instanceof Map)) { + console.error('objectConfigurationSources is not a Map'); + return undefined; + } + + for (const propertyKey of Object.keys(configurationValueObject)) { + const propertyDefaultValue = configurationValueObject[propertyKey]; + + const isObjectSetting = types.isObject(propertyDefaultValue) && + (types.isUndefined(defaultValue[propertyKey]) || types.isObject(defaultValue[propertyKey])); + + // If the default value is an object, merge the objects and store the source of each keys + if (isObjectSetting) { + defaultValue[propertyKey] = { ...(defaultValue[propertyKey] ?? {}), ...propertyDefaultValue }; + // Track the source of each value in the object + if (valueSource) { + for (const objectKey in propertyDefaultValue) { + source.set(`${propertyKey}.${objectKey}`, valueSource); + } + } + } + + // Primitive values are overridden + else { + defaultValue[propertyKey] = propertyDefaultValue; + if (valueSource) { + source.set(propertyKey, valueSource); + } else { + source.delete(propertyKey); + } } } - this.updateOverridePropertyPatternKey(); + return { value: defaultValue, source }; + } + + private mergeDefaultConfigurationsForConfigurationProperty(propertyKey: string, value: any, valuesSource: IExtensionInfo | undefined, existingDefaultOverride: IConfigurationDefaultOverrideValue | undefined): IConfigurationDefaultOverrideValue | undefined { + const property = this.configurationProperties[propertyKey]; + const existingDefaultValue = existingDefaultOverride?.value ?? property?.defaultDefaultValue; + let source: ConfigurationDefaultValueSource | undefined = valuesSource; + + const isObjectSetting = types.isObject(value) && + ( + property !== undefined && property.type === 'object' || + property === undefined && (types.isUndefined(existingDefaultValue) || types.isObject(existingDefaultValue)) + ); + + // If the default value is an object, merge the objects and store the source of each keys + if (isObjectSetting) { + source = existingDefaultOverride?.source ?? new Map(); + + // This should not happen + if (!(source instanceof Map)) { + console.error('defaultValueSource is not a Map'); + return undefined; + } + + for (const objectKey in value) { + if (valuesSource) { + source.set(`${propertyKey}.${objectKey}`, valuesSource); + } + } + value = { ...(types.isObject(existingDefaultValue) ? existingDefaultValue : {}), ...value }; + } + + return { value, source }; } public deltaConfiguration(delta: IConfigurationDelta): void { @@ -569,8 +706,18 @@ class ConfigurationRegistry implements IConfigurationRegistry { return this.excludedConfigurationProperties; } - getConfigurationDefaultsOverrides(): Map { - return this.configurationDefaultsOverrides; + getRegisteredDefaultConfigurations(): IConfigurationDefaults[] { + return [...this.registeredConfigurationDefaults]; + } + + getConfigurationDefaultsOverrides(): Map { + const configurationDefaultsOverrides = new Map(); + for (const [key, value] of this.configurationDefaultsOverrides) { + if (value.configurationDefaultOverrideValue) { + configurationDefaultsOverrides.set(key, value.configurationDefaultOverrideValue); + } + } + return configurationDefaultsOverrides; } private registerJSONConfiguration(configuration: IConfigurationNode) { @@ -671,9 +818,15 @@ class ConfigurationRegistry implements IConfigurationRegistry { } private updatePropertyDefaultValue(key: string, property: IRegisteredConfigurationPropertySchema): void { - const configurationdefaultOverride = this.configurationDefaultsOverrides.get(key); - let defaultValue = configurationdefaultOverride?.value; - let defaultSource = configurationdefaultOverride?.source; + const configurationdefaultOverride = this.configurationDefaultsOverrides.get(key)?.configurationDefaultOverrideValue; + let defaultValue = undefined; + let defaultSource = undefined; + if (configurationdefaultOverride + && (!property.disallowConfigurationDefault || !configurationdefaultOverride.source) // Prevent overriding the default value if the property is disallowed to be overridden by configuration defaults from extensions + ) { + defaultValue = configurationdefaultOverride.value; + defaultSource = configurationdefaultOverride.source; + } if (types.isUndefined(defaultValue)) { defaultValue = property.defaultDefaultValue; defaultSource = undefined; @@ -758,3 +911,36 @@ export function getScopes(): [string, ConfigurationScope | undefined][] { scopes.push(['task', ConfigurationScope.RESOURCE]); return scopes; } + +export function getAllConfigurationProperties(configurationNode: IConfigurationNode[]): IStringDictionary { + const result: IStringDictionary = {}; + for (const configuration of configurationNode) { + const properties = configuration.properties; + if (types.isObject(properties)) { + for (const key in properties) { + result[key] = properties[key]; + } + } + if (configuration.allOf) { + Object.assign(result, getAllConfigurationProperties(configuration.allOf)); + } + } + return result; +} + +export function parseScope(scope: string): ConfigurationScope { + switch (scope) { + case 'application': + return ConfigurationScope.APPLICATION; + case 'machine': + return ConfigurationScope.MACHINE; + case 'resource': + return ConfigurationScope.RESOURCE; + case 'machine-overridable': + return ConfigurationScope.MACHINE_OVERRIDABLE; + case 'language-overridable': + return ConfigurationScope.LANGUAGE_OVERRIDABLE; + default: + return ConfigurationScope.WINDOW; + } +} diff --git a/src/vs/platform/configuration/common/configurations.ts b/src/vs/platform/configuration/common/configurations.ts index d6f09a4d176..5e3c303ada6 100644 --- a/src/vs/platform/configuration/common/configurations.ts +++ b/src/vs/platform/configuration/common/configurations.ts @@ -61,9 +61,9 @@ export class DefaultConfiguration extends Disposable { const defaultOverrideValue = configurationDefaultsOverrides[key]; const propertySchema = configurationProperties[key]; if (defaultOverrideValue !== undefined) { - this._configurationModel.addValue(key, defaultOverrideValue); + this._configurationModel.setValue(key, defaultOverrideValue); } else if (propertySchema) { - this._configurationModel.addValue(key, propertySchema.default); + this._configurationModel.setValue(key, propertySchema.default); } else { this._configurationModel.removeValue(key); } diff --git a/src/vs/platform/configuration/test/common/configuration.test.ts b/src/vs/platform/configuration/test/common/configuration.test.ts index e7b169e7341..1f709cf2c0f 100644 --- a/src/vs/platform/configuration/test/common/configuration.test.ts +++ b/src/vs/platform/configuration/test/common/configuration.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { merge, removeFromValueTree } from 'vs/platform/configuration/common/configuration'; import { mergeChanges } from 'vs/platform/configuration/common/configurationModels'; diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index 50b612d83d9..ab425c4fd30 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ResourceMap } from 'vs/base/common/map'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/configuration/test/common/configurationRegistry.test.ts b/src/vs/platform/configuration/test/common/configurationRegistry.test.ts index 9fc9e709322..0f3bf7e2d87 100644 --- a/src/vs/platform/configuration/test/common/configurationRegistry.test.ts +++ b/src/vs/platform/configuration/test/common/configurationRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -14,6 +14,14 @@ suite('ConfigurationRegistry', () => { const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); + setup(() => reset()); + teardown(() => reset()); + + function reset() { + configurationRegistry.deregisterConfigurations(configurationRegistry.getConfigurations()); + configurationRegistry.deregisterDefaultConfigurations(configurationRegistry.getRegisteredDefaultConfigurations()); + } + test('configuration override', async () => { configurationRegistry.registerConfiguration({ 'id': '_test_default', @@ -31,6 +39,24 @@ suite('ConfigurationRegistry', () => { assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, c: 3 }); }); + test('configuration override defaults - prevent overriding default value', async () => { + configurationRegistry.registerConfiguration({ + 'id': '_test_default', + 'type': 'object', + 'properties': { + 'config.preventDefaultValueOverride': { + 'type': 'object', + default: { a: 0 }, + 'disallowConfigurationDefault': true + } + } + }); + + configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config.preventDefaultValueOverride': { a: 1, b: 2 } } }]); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config.preventDefaultValueOverride'].default, { a: 0 }); + }); + test('configuration override defaults - merges defaults', async () => { configurationRegistry.registerDefaultConfigurations([{ overrides: { '[lang]': { a: 1, b: 2 } } }]); configurationRegistry.registerDefaultConfigurations([{ overrides: { '[lang]': { a: 2, c: 3 } } }]); @@ -38,7 +64,7 @@ suite('ConfigurationRegistry', () => { assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, b: 2, c: 3 }); }); - test('configuration defaults - overrides defaults', async () => { + test('configuration defaults - merge object default overrides', async () => { configurationRegistry.registerConfiguration({ 'id': '_test_default', 'type': 'object', @@ -51,7 +77,7 @@ suite('ConfigurationRegistry', () => { configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 1, b: 2 } } }]); configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 2, c: 3 } } }]); - assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, c: 3 }); + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, b: 2, c: 3 }); }); test('registering multiple settings with same policy', async () => { @@ -79,4 +105,88 @@ suite('ConfigurationRegistry', () => { assert.ok(actual['policy1'] !== undefined); assert.ok(actual['policy2'] === undefined); }); + + test('configuration defaults - deregister merged object default override', async () => { + configurationRegistry.registerConfiguration({ + 'id': '_test_default', + 'type': 'object', + 'properties': { + 'config': { + 'type': 'object', + } + } + }); + + const overrides1 = [{ overrides: { 'config': { a: 1, b: 2 } }, source: { id: 'source1', displayName: 'source1' } }]; + const overrides2 = [{ overrides: { 'config': { a: 2, c: 3 } }, source: { id: 'source2', displayName: 'source2' } }]; + + configurationRegistry.registerDefaultConfigurations(overrides1); + configurationRegistry.registerDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, b: 2, c: 3 }); + + configurationRegistry.deregisterDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 1, b: 2 }); + + configurationRegistry.deregisterDefaultConfigurations(overrides1); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, {}); + }); + + test('configuration defaults - deregister merged object default override without source', async () => { + configurationRegistry.registerConfiguration({ + 'id': '_test_default', + 'type': 'object', + 'properties': { + 'config': { + 'type': 'object', + } + } + }); + + const overrides1 = [{ overrides: { 'config': { a: 1, b: 2 } } }]; + const overrides2 = [{ overrides: { 'config': { a: 2, c: 3 } } }]; + + configurationRegistry.registerDefaultConfigurations(overrides1); + configurationRegistry.registerDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, b: 2, c: 3 }); + + configurationRegistry.deregisterDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 1, b: 2 }); + + configurationRegistry.deregisterDefaultConfigurations(overrides1); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, {}); + }); + + test('configuration defaults - deregister merged object default language overrides', async () => { + configurationRegistry.registerConfiguration({ + 'id': '_test_default', + 'type': 'object', + 'properties': { + 'config': { + 'type': 'object', + } + } + }); + + const overrides1 = [{ overrides: { '[lang]': { 'config': { a: 1, b: 2 } } }, source: { id: 'source1', displayName: 'source1' } }]; + const overrides2 = [{ overrides: { '[lang]': { 'config': { a: 2, c: 3 } } }, source: { id: 'source2', displayName: 'source2' } }]; + + configurationRegistry.registerDefaultConfigurations(overrides1); + configurationRegistry.registerDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { 'config': { a: 2, b: 2, c: 3 } }); + + configurationRegistry.deregisterDefaultConfigurations(overrides2); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { 'config': { a: 1, b: 2 } }); + + configurationRegistry.deregisterDefaultConfigurations(overrides1); + + assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'], undefined); + }); }); diff --git a/src/vs/platform/configuration/test/common/configurationService.test.ts b/src/vs/platform/configuration/test/common/configurationService.test.ts index 880e49e8e48..0eff504b9bd 100644 --- a/src/vs/platform/configuration/test/common/configurationService.test.ts +++ b/src/vs/platform/configuration/test/common/configurationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/platform/configuration/test/common/configurations.test.ts b/src/vs/platform/configuration/test/common/configurations.test.ts index 378107be505..5b237b73719 100644 --- a/src/vs/platform/configuration/test/common/configurations.test.ts +++ b/src/vs/platform/configuration/test/common/configurations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { equals } from 'vs/base/common/objects'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -22,8 +22,7 @@ suite('DefaultConfiguration', () => { function reset() { configurationRegistry.deregisterConfigurations(configurationRegistry.getConfigurations()); - const configurationDefaultsOverrides = configurationRegistry.getConfigurationDefaultsOverrides(); - configurationRegistry.deregisterDefaultConfigurations([...configurationDefaultsOverrides.keys()].map(key => ({ extensionId: configurationDefaultsOverrides.get(key)?.source, overrides: { [key]: configurationDefaultsOverrides.get(key)?.value } }))); + configurationRegistry.deregisterDefaultConfigurations(configurationRegistry.getRegisteredDefaultConfigurations()); } test('Test registering a property before initialize', async () => { @@ -110,7 +109,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('a'), { b: { c: '2' } })); assert.ok(equals(actual.contents, { 'a': { b: { c: '2' } } })); - assert.deepStrictEqual(actual.keys, ['a.b', 'a.b.c']); + assert.deepStrictEqual(actual.keys.sort(), ['a.b', 'a.b.c']); }); test('Test registering the same property again', async () => { @@ -158,7 +157,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); }); @@ -191,7 +190,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['b', '[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); }); @@ -227,7 +226,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['[a]', 'b']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); assert.deepStrictEqual(properties, ['b']); }); @@ -263,7 +262,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['b', '[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); assert.deepStrictEqual(properties, ['[a]']); }); @@ -299,7 +298,7 @@ suite('DefaultConfiguration', () => { assert.ok(equals(actual.getValue('[a]'), { 'b': true })); assert.ok(equals(actual.contents, { 'b': false, '[a]': { 'b': true } })); assert.ok(equals(actual.overrides, [{ contents: { 'b': true }, identifiers: ['a'], keys: ['b'] }])); - assert.deepStrictEqual(actual.keys, ['b', '[a]']); + assert.deepStrictEqual(actual.keys.sort(), ['[a]', 'b']); assert.strictEqual(actual.getOverrideValue('b', 'a'), true); }); @@ -361,4 +360,54 @@ suite('DefaultConfiguration', () => { assert.deepStrictEqual(testObject.configurationModel.keys, ['b']); assert.strictEqual(testObject.configurationModel.getOverrideValue('b', 'a'), undefined); }); + + test('Test deregistering a merged language object setting', async () => { + const testObject = disposables.add(new DefaultConfiguration(new NullLogService())); + configurationRegistry.registerConfiguration({ + 'id': 'b', + 'order': 1, + 'title': 'b', + 'type': 'object', + 'properties': { + 'b': { + 'description': 'b', + 'type': 'object', + 'default': {}, + } + } + }); + const node1 = { + overrides: { + '[a]': { + 'b': { + 'aa': '1', + 'bb': '2' + } + } + }, + source: { id: 'source1', displayName: 'source1' } + }; + + const node2 = { + overrides: { + '[a]': { + 'b': { + 'bb': '20', + 'cc': '30' + } + } + }, + source: { id: 'source2', displayName: 'source2' } + }; + configurationRegistry.registerDefaultConfigurations([node1]); + configurationRegistry.registerDefaultConfigurations([node2]); + await testObject.initialize(); + + configurationRegistry.deregisterDefaultConfigurations([node1]); + assert.ok(equals(testObject.configurationModel.getValue('[a]'), { 'b': { 'bb': '20', 'cc': '30' } })); + assert.ok(equals(testObject.configurationModel.contents, { '[a]': { 'b': { 'bb': '20', 'cc': '30' } }, 'b': {} })); + assert.ok(equals(testObject.configurationModel.overrides, [{ contents: { 'b': { 'bb': '20', 'cc': '30' } }, identifiers: ['a'], keys: ['b'] }])); + assert.deepStrictEqual(testObject.configurationModel.keys.sort(), ['[a]', 'b']); + assert.ok(equals(testObject.configurationModel.getOverrideValue('b', 'a'), { 'bb': '20', 'cc': '30' })); + }); }); diff --git a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts index 94ee037eb5d..d3e44993618 100644 --- a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts +++ b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { DefaultConfiguration, PolicyConfiguration } from 'vs/platform/configuration/common/configurations'; diff --git a/src/vs/platform/contextkey/test/browser/contextkey.test.ts b/src/vs/platform/contextkey/test/browser/contextkey.test.ts index b56d4b874da..d2301c19147 100644 --- a/src/vs/platform/contextkey/test/browser/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/browser/contextkey.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DeferredPromise } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 8f388fb13ee..2555701c1d8 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ContextKeyExpr, ContextKeyExpression, implies } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/platform/contextkey/test/common/parser.test.ts b/src/vs/platform/contextkey/test/common/parser.test.ts index c5be2596341..17bfa468ec9 100644 --- a/src/vs/platform/contextkey/test/common/parser.test.ts +++ b/src/vs/platform/contextkey/test/common/parser.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Parser } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/platform/contextkey/test/common/scanner.test.ts b/src/vs/platform/contextkey/test/common/scanner.test.ts index df897db9e8a..dacbfbebbdd 100644 --- a/src/vs/platform/contextkey/test/common/scanner.test.ts +++ b/src/vs/platform/contextkey/test/common/scanner.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Scanner, Token, TokenType } from 'vs/platform/contextkey/common/scanner'; diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index 55e0d50422d..abc08f92252 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; -import { $, addDisposableListener, EventType, getActiveElement, getWindow, isAncestor } from 'vs/base/browser/dom'; +import { $, addDisposableListener, EventType, getActiveElement, getWindow, isAncestor, isHTMLElement } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Menu } from 'vs/base/browser/ui/menu/menu'; import { ActionRunner, IRunEvent, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; @@ -49,7 +49,7 @@ export class ContextMenuHandler { let menu: Menu | undefined; - const shadowRootElement = delegate.domForShadowRoot instanceof HTMLElement ? delegate.domForShadowRoot : undefined; + const shadowRootElement = isHTMLElement(delegate.domForShadowRoot) ? delegate.domForShadowRoot : undefined; this.contextViewService.showContextView({ getAnchor: () => delegate.getAnchor(), canRelayout: false, diff --git a/src/vs/platform/contextview/browser/contextMenuService.ts b/src/vs/platform/contextview/browser/contextMenuService.ts index 907cf9449a8..f70e349945e 100644 --- a/src/vs/platform/contextview/browser/contextMenuService.ts +++ b/src/vs/platform/contextview/browser/contextMenuService.ts @@ -86,9 +86,8 @@ export namespace ContextMenuMenuDelegate { getActions: () => { const target: IAction[] = []; if (menuId) { - const menu = menuService.createMenu(menuId, contextKeyService ?? globalContextKeyService); - createAndFillInContextMenuActions(menu, menuActionOptions, target); - menu.dispose(); + const menu = menuService.getMenuActions(menuId, contextKeyService ?? globalContextKeyService, menuActionOptions); + createAndFillInContextMenuActions(menu, target); } if (!delegate.getActions) { return target; diff --git a/src/vs/platform/diagnostics/common/diagnostics.ts b/src/vs/platform/diagnostics/common/diagnostics.ts index 7d5af4f92da..fb30b762cef 100644 --- a/src/vs/platform/diagnostics/common/diagnostics.ts +++ b/src/vs/platform/diagnostics/common/diagnostics.ts @@ -80,6 +80,8 @@ export interface WorkspaceStats { fileCount: number; maxFilesReached: boolean; launchConfigFiles: WorkspaceStatItem[]; + totalScanTime: number; + totalReaddirCount: number; } export interface PerformanceInfo { diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 7ca4d9c278c..7e0bc1170e0 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -2,6 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; import * as osLib from 'os'; import { Promises } from 'vs/base/common/async'; import { getNodeType, parse, ParseError } from 'vs/base/common/json'; @@ -9,6 +11,7 @@ import { Schemas } from 'vs/base/common/network'; import { basename, join } from 'vs/base/common/path'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { ProcessItem } from 'vs/base/common/processes'; +import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; import { virtualMachineHint } from 'vs/base/node/id'; import { IDirent, Promises as pfs } from 'vs/base/node/pfs'; @@ -60,11 +63,13 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P const MAX_FILES = 20000; - function collect(root: string, dir: string, filter: string[], token: { count: number; maxReached: boolean }): Promise { + function collect(root: string, dir: string, filter: string[], token: { count: number; maxReached: boolean; readdirCount: number }): Promise { const relativePath = dir.substring(root.length + 1); return Promises.withAsyncBody(async resolve => { let files: IDirent[]; + + token.readdirCount++; try { files = await pfs.readdir(dir, { withFileTypes: true }); } catch (error) { @@ -130,8 +135,8 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P } const statsPromise = Promises.withAsyncBody(async (resolve) => { - const token: { count: number; maxReached: boolean } = { count: 0, maxReached: false }; - + const token: { count: number; maxReached: boolean; readdirCount: number } = { count: 0, maxReached: false, readdirCount: 0 }; + const sw = new StopWatch(true); await collect(folder, folder, filter, token); const launchConfigs = await collectLaunchConfigs(folder); resolve({ @@ -139,7 +144,9 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P fileTypes: asSortedItems(fileTypes), fileCount: token.count, maxFilesReached: token.maxReached, - launchConfigFiles: launchConfigs + launchConfigFiles: launchConfigs, + totalScanTime: sw.elapsed(), + totalReaddirCount: token.readdirCount }); }); @@ -173,7 +180,7 @@ export async function collectLaunchConfigs(folder: string): Promise(); const launchConfig = join(folder, '.vscode', 'launch.json'); - const contents = await pfs.readFile(launchConfig); + const contents = await fs.promises.readFile(launchConfig); const errors: ParseError[] = []; const json = parse(contents.toString(), errors); @@ -568,6 +575,23 @@ export class DiagnosticsService implements IDiagnosticsService { count: e.count }); }); + + // Workspace stats metadata + type WorkspaceStatsMetadataClassification = { + owner: 'jrieken'; + comment: 'Metadata about workspace metadata collection'; + duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How did it take to make workspace stats' }; + reachedLimit: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Did making workspace stats reach its limits' }; + fileCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How many files did workspace stats discover' }; + readdirCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'How many readdir call were needed' }; + }; + type WorkspaceStatsMetadata = { + duration: number; + reachedLimit: boolean; + fileCount: number; + readdirCount: number; + }; + this.telemetryService.publicLog2('workspace.stats.metadata', { duration: stats.totalScanTime, reachedLimit: stats.maxFilesReached, fileCount: stats.fileCount, readdirCount: stats.totalReaddirCount }); } catch { // Report nothing if collecting metadata fails. } diff --git a/src/vs/platform/dialogs/electron-main/dialogMainService.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts index bc1230a48ea..2fffe78c5fb 100644 --- a/src/vs/platform/dialogs/electron-main/dialogMainService.ts +++ b/src/vs/platform/dialogs/electron-main/dialogMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, dialog, FileFilter, MessageBoxOptions, MessageBoxReturnValue, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'; +import electron from 'electron'; import { Queue } from 'vs/base/common/async'; import { hash } from 'vs/base/common/hash'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; @@ -24,14 +24,14 @@ export interface IDialogMainService { readonly _serviceBrand: undefined; - pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; - pickFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; - pickFile(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; - pickWorkspace(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; + pickFileFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise; + pickFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise; + pickFile(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise; + pickWorkspace(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise; - showMessageBox(options: MessageBoxOptions, window?: BrowserWindow): Promise; - showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise; - showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise; + showMessageBox(options: electron.MessageBoxOptions, window?: electron.BrowserWindow): Promise; + showSaveDialog(options: electron.SaveDialogOptions, window?: electron.BrowserWindow): Promise; + showOpenDialog(options: electron.OpenDialogOptions, window?: electron.BrowserWindow): Promise; } interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions { @@ -40,7 +40,7 @@ interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions { readonly title: string; readonly buttonLabel?: string; - readonly filters?: FileFilter[]; + readonly filters?: electron.FileFilter[]; } export class DialogMainService implements IDialogMainService { @@ -48,8 +48,8 @@ export class DialogMainService implements IDialogMainService { declare readonly _serviceBrand: undefined; private readonly windowFileDialogLocks = new Map>(); - private readonly windowDialogQueues = new Map>(); - private readonly noWindowDialogueQueue = new Queue(); + private readonly windowDialogQueues = new Map>(); + private readonly noWindowDialogueQueue = new Queue(); constructor( @ILogService private readonly logService: ILogService, @@ -57,19 +57,19 @@ export class DialogMainService implements IDialogMainService { ) { } - pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + pickFileFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise { return this.doPick({ ...options, pickFolders: true, pickFiles: true, title: localize('open', "Open") }, window); } - pickFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + pickFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise { return this.doPick({ ...options, pickFolders: true, title: localize('openFolder', "Open Folder") }, window); } - pickFile(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + pickFile(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise { return this.doPick({ ...options, pickFiles: true, title: localize('openFile', "Open File") }, window); } - pickWorkspace(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + pickWorkspace(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise { const title = localize('openWorkspaceTitle', "Open Workspace from File"); const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")); const filters = WORKSPACE_FILTER; @@ -77,10 +77,10 @@ export class DialogMainService implements IDialogMainService { return this.doPick({ ...options, pickFiles: true, title, filters, buttonLabel }, window); } - private async doPick(options: IInternalNativeOpenDialogOptions, window?: BrowserWindow): Promise { + private async doPick(options: IInternalNativeOpenDialogOptions, window?: electron.BrowserWindow): Promise { // Ensure dialog options - const dialogOptions: OpenDialogOptions = { + const dialogOptions: electron.OpenDialogOptions = { title: options.title, buttonLabel: options.buttonLabel, filters: options.filters, @@ -105,7 +105,7 @@ export class DialogMainService implements IDialogMainService { } // Show Dialog - const result = await this.showOpenDialog(dialogOptions, (window || BrowserWindow.getFocusedWindow()) ?? undefined); + const result = await this.showOpenDialog(dialogOptions, (window || electron.BrowserWindow.getFocusedWindow()) ?? undefined); if (result && result.filePaths && result.filePaths.length > 0) { return result.filePaths; } @@ -113,14 +113,14 @@ export class DialogMainService implements IDialogMainService { return undefined; } - private getWindowDialogQueue(window?: BrowserWindow): Queue { + private getWindowDialogQueue(window?: electron.BrowserWindow): Queue { // Queue message box requests per window so that one can show // after the other. if (window) { let windowDialogQueue = this.windowDialogQueues.get(window.id); if (!windowDialogQueue) { - windowDialogQueue = new Queue(); + windowDialogQueue = new Queue(); this.windowDialogQueues.set(window.id, windowDialogQueue); } @@ -130,15 +130,15 @@ export class DialogMainService implements IDialogMainService { } } - showMessageBox(rawOptions: MessageBoxOptions, window?: BrowserWindow): Promise { - return this.getWindowDialogQueue(window).queue(async () => { + showMessageBox(rawOptions: electron.MessageBoxOptions, window?: electron.BrowserWindow): Promise { + return this.getWindowDialogQueue(window).queue(async () => { const { options, buttonIndeces } = massageMessageBoxOptions(rawOptions, this.productService); - let result: MessageBoxReturnValue | undefined = undefined; + let result: electron.MessageBoxReturnValue | undefined = undefined; if (window) { - result = await dialog.showMessageBox(window, options); + result = await electron.dialog.showMessageBox(window, options); } else { - result = await dialog.showMessageBox(options); + result = await electron.dialog.showMessageBox(options); } return { @@ -148,7 +148,7 @@ export class DialogMainService implements IDialogMainService { }); } - async showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise { + async showSaveDialog(options: electron.SaveDialogOptions, window?: electron.BrowserWindow): Promise { // Prevent duplicates of the same dialog queueing at the same time const fileDialogLock = this.acquireFileDialogLock(options, window); @@ -159,12 +159,12 @@ export class DialogMainService implements IDialogMainService { } try { - return await this.getWindowDialogQueue(window).queue(async () => { - let result: SaveDialogReturnValue; + return await this.getWindowDialogQueue(window).queue(async () => { + let result: electron.SaveDialogReturnValue; if (window) { - result = await dialog.showSaveDialog(window, options); + result = await electron.dialog.showSaveDialog(window, options); } else { - result = await dialog.showSaveDialog(options); + result = await electron.dialog.showSaveDialog(options); } result.filePath = this.normalizePath(result.filePath); @@ -190,7 +190,7 @@ export class DialogMainService implements IDialogMainService { return paths.map(path => this.normalizePath(path)); } - async showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise { + async showOpenDialog(options: electron.OpenDialogOptions, window?: electron.BrowserWindow): Promise { // Ensure the path exists (if provided) if (options.defaultPath) { @@ -209,12 +209,12 @@ export class DialogMainService implements IDialogMainService { } try { - return await this.getWindowDialogQueue(window).queue(async () => { - let result: OpenDialogReturnValue; + return await this.getWindowDialogQueue(window).queue(async () => { + let result: electron.OpenDialogReturnValue; if (window) { - result = await dialog.showOpenDialog(window, options); + result = await electron.dialog.showOpenDialog(window, options); } else { - result = await dialog.showOpenDialog(options); + result = await electron.dialog.showOpenDialog(options); } result.filePaths = this.normalizePaths(result.filePaths); @@ -226,7 +226,7 @@ export class DialogMainService implements IDialogMainService { } } - private acquireFileDialogLock(options: SaveDialogOptions | OpenDialogOptions, window?: BrowserWindow): IDisposable | undefined { + private acquireFileDialogLock(options: electron.SaveDialogOptions | electron.OpenDialogOptions, window?: electron.BrowserWindow): IDisposable | undefined { // If no window is provided, allow as many dialogs as // needed since we consider them not modal per window diff --git a/src/vs/platform/dnd/browser/dnd.ts b/src/vs/platform/dnd/browser/dnd.ts index 1bd55afefaf..b742b3ae448 100644 --- a/src/vs/platform/dnd/browser/dnd.ts +++ b/src/vs/platform/dnd/browser/dnd.ts @@ -201,6 +201,7 @@ async function extractFilesDropData(accessor: ServicesAccessor, event: DragEvent async function extractFileTransferData(accessor: ServicesAccessor, items: DataTransferItemList): Promise { const fileSystemProvider = accessor.get(IFileService).getProvider(Schemas.file); + // eslint-disable-next-line no-restricted-syntax if (!(fileSystemProvider instanceof HTMLFileSystemProvider)) { return []; // only supported when running in web } diff --git a/src/vs/platform/environment/electron-main/environmentMainService.ts b/src/vs/platform/environment/electron-main/environmentMainService.ts index 748ff075783..dec04406afa 100644 --- a/src/vs/platform/environment/electron-main/environmentMainService.ts +++ b/src/vs/platform/environment/electron-main/environmentMainService.ts @@ -19,9 +19,6 @@ export const IEnvironmentMainService = refineServiceDecorator = {}; - @memoize - get cachedLanguagesPath(): string { return join(this.userDataPath, 'clp'); } - @memoize get backupHome(): string { return join(this.userDataPath, 'Backups'); } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 26b7d9c6937..16a942afe05 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as minimist from 'minimist'; +import minimist from 'minimist'; import { isWindows } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; diff --git a/src/vs/platform/environment/node/argvHelper.ts b/src/vs/platform/environment/node/argvHelper.ts index d8cefb6df67..a94fca911ea 100644 --- a/src/vs/platform/environment/node/argvHelper.ts +++ b/src/vs/platform/environment/node/argvHelper.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; diff --git a/src/vs/platform/environment/node/stdin.ts b/src/vs/platform/environment/node/stdin.ts index 56ab151407d..b3e71e04493 100644 --- a/src/vs/platform/environment/node/stdin.ts +++ b/src/vs/platform/environment/node/stdin.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { tmpdir } from 'os'; import { Queue } from 'vs/base/common/async'; import { randomPath } from 'vs/base/common/extpath'; -import { Promises } from 'vs/base/node/pfs'; import { resolveTerminalEncoding } from 'vs/base/node/terminalEncoding'; export function hasStdinWithoutTty() { @@ -43,7 +43,7 @@ export async function readFromStdin(targetPath: string, verbose: boolean, onEnd? let [encoding, iconv] = await Promise.all([ resolveTerminalEncoding(verbose), // respect terminal encoding when piping into file import('@vscode/iconv-lite-umd'), // lazy load encoding module for usage - Promises.appendFile(targetPath, '') // make sure file exists right away (https://github.com/microsoft/vscode/issues/155341) + fs.promises.appendFile(targetPath, '') // make sure file exists right away (https://github.com/microsoft/vscode/issues/155341) ]); if (!iconv.encodingExists(encoding)) { @@ -63,7 +63,7 @@ export async function readFromStdin(targetPath: string, verbose: boolean, onEnd? process.stdin.on('data', chunk => { const chunkStr = decoder.write(chunk); - appendFileQueue.queue(() => Promises.appendFile(targetPath, chunkStr)); + appendFileQueue.queue(() => fs.promises.appendFile(targetPath, chunkStr)); }); process.stdin.on('end', () => { @@ -72,7 +72,7 @@ export async function readFromStdin(targetPath: string, verbose: boolean, onEnd? appendFileQueue.queue(async () => { try { if (typeof end === 'string') { - await Promises.appendFile(targetPath, end); + await fs.promises.appendFile(targetPath, end); } } finally { onEnd?.(); diff --git a/src/vs/platform/environment/node/userDataPath.js b/src/vs/platform/environment/node/userDataPath.js index 92898523ed1..1e89f1fee06 100644 --- a/src/vs/platform/environment/node/userDataPath.js +++ b/src/vs/platform/environment/node/userDataPath.js @@ -10,8 +10,10 @@ 'use strict'; /** - * @typedef {import('../../environment/common/argv').NativeParsedArgs} NativeParsedArgs - * + * @import { NativeParsedArgs } from '../../environment/common/argv' + */ + + /** * @param {typeof import('path')} path * @param {typeof import('os')} os * @param {string} cwd diff --git a/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts b/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts index 78fd7354520..268f5ce52bb 100644 --- a/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts +++ b/src/vs/platform/environment/test/electron-main/environmentMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import product from 'vs/platform/product/common/product'; import { isLinux } from 'vs/base/common/platform'; diff --git a/src/vs/platform/environment/test/node/argv.test.ts b/src/vs/platform/environment/test/node/argv.test.ts index a188b52b711..a82be9607d0 100644 --- a/src/vs/platform/environment/test/node/argv.test.ts +++ b/src/vs/platform/environment/test/node/argv.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { formatOptions, Option, OptionDescriptions, Subcommand, parseArgs, ErrorReporter } from 'vs/platform/environment/node/argv'; import { addArg } from 'vs/platform/environment/node/argvHelper'; diff --git a/src/vs/platform/environment/test/node/environmentService.test.ts b/src/vs/platform/environment/test/node/environmentService.test.ts index ffe418fc702..6f256621040 100644 --- a/src/vs/platform/environment/test/node/environmentService.test.ts +++ b/src/vs/platform/environment/test/node/environmentService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseExtensionHostDebugPort } from 'vs/platform/environment/common/environmentService'; import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; diff --git a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts index 68128a214fd..81db8b47267 100644 --- a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts +++ b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { flakySuite } from 'vs/base/test/common/testUtils'; @@ -19,7 +19,7 @@ flakySuite('Native Modules (all platforms)', () => { }); test('native-is-elevated', async () => { - const isElevated = await import('native-is-elevated'); + const isElevated = (await import('native-is-elevated')).default; assert.ok(typeof isElevated === 'function', testErrorMessage('native-is-elevated ')); const result = isElevated(); diff --git a/src/vs/platform/environment/test/node/userDataPath.test.ts b/src/vs/platform/environment/test/node/userDataPath.test.ts index 644260cff8f..72278e46ac0 100644 --- a/src/vs/platform/environment/test/node/userDataPath.test.ts +++ b/src/vs/platform/environment/test/node/userDataPath.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; import { getUserDataPath } from 'vs/platform/environment/node/userDataPath'; diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 2ac86759fb9..f3ae52b16a2 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -18,10 +18,12 @@ import { IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, IProductVersion, ExtensionGalleryErrorCode, - EXTENSION_INSTALL_SOURCE_CONTEXT + EXTENSION_INSTALL_SOURCE_CONTEXT, + DidUpdateExtensionMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { areApiProposalsCompatible } from 'vs/platform/extensions/common/extensionValidator'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -73,7 +75,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl protected _onDidUninstallExtension = this._register(new Emitter()); get onDidUninstallExtension() { return this._onDidUninstallExtension.event; } - protected readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); + protected readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); get onDidUpdateExtensionMetadata() { return this._onDidUpdateExtensionMetadata.event; } private readonly participants: IExtensionManagementParticipant[] = []; @@ -129,7 +131,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion, options.productVersion ?? { version: this.productService.version, date: this.productService.date }); installableExtensions.push({ ...compatible, options }); } catch (error) { - results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error }); + results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error, profileLocation: options.profileLocation ?? this.getCurrentExtensionsManifestLocation() }); } })); @@ -160,7 +162,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const existing = (await this.getInstalled(ExtensionType.User, profile.extensionsResource)) .find(e => areSameExtensions(e.identifier, extension.identifier)); if (existing) { - this._onDidUpdateExtensionMetadata.fire(existing); + this._onDidUpdateExtensionMetadata.fire({ local: existing, profileLocation: profile.extensionsResource }); } else { this._onDidUninstallExtension.fire({ identifier: extension.identifier, profileLocation: profile.extensionsResource }); } @@ -194,6 +196,24 @@ export abstract class AbstractExtensionManagementService extends Disposable impl this.participants.push(participant); } + async resetPinnedStateForAllUserExtensions(pinned: boolean): Promise { + try { + await this.joinAllSettled(this.userDataProfilesService.profiles.map( + async profile => { + const extensions = await this.getInstalled(ExtensionType.User, profile.extensionsResource); + await this.joinAllSettled(extensions.map( + async extension => { + if (extension.pinned !== pinned) { + await this.updateMetadata(extension, { pinned }, profile.extensionsResource); + } + })); + })); + } catch (error) { + this.logService.error('Error while resetting pinned state for all user extensions', getErrorMessage(error)); + throw error; + } + } + protected async installExtensions(extensions: InstallableExtension[]): Promise { const installExtensionResultsMap = new Map(); const installingExtensionsMap = new Map(); @@ -205,7 +225,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const key = `${getGalleryExtensionId(manifest.publisher, manifest.name)}-${options.profileLocation.toString()}`; installingExtensionsMap.set(key, { task: installExtensionTask, root }); this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension, profileLocation: options.profileLocation }); - this.logService.info('Installing extension:', installExtensionTask.identifier.id, options.profileLocation.toString()); + this.logService.info('Installing extension:', installExtensionTask.identifier.id, options); // only cache gallery extensions tasks if (!URI.isUri(extension)) { this.installingExtensions.set(getInstallExtensionTaskKey(extension, options.profileLocation), { task: installExtensionTask, waitingTasks: [] }); @@ -547,6 +567,10 @@ export abstract class AbstractExtensionManagementService extends Disposable impl compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease, productVersion); if (!compatibleExtension) { + const incompatibleApiProposalsMessages: string[] = []; + if (!areApiProposalsCompatible(extension.properties.enabledApiProposals ?? [], incompatibleApiProposalsMessages)) { + throw new ExtensionManagementError(nls.localize('incompatibleAPI', "Can't install '{0}' extension. {1}", extension.displayName ?? extension.identifier.id, incompatibleApiProposalsMessages[0]), ExtensionManagementErrorCode.IncompatibleApi); + } /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { throw new ExtensionManagementError(nls.localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.displayName ?? extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound); @@ -784,7 +808,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract reinstallFromGallery(extension: ILocalExtension): Promise; abstract cleanUp(): Promise; - abstract updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; + abstract updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise; protected abstract getCurrentExtensionsManifestLocation(): URI; protected abstract createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask; diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index cc587b5770e..e8698264bfa 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -18,7 +18,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; +import { areApiProposalsCompatible, isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -209,6 +209,7 @@ const PropertyType = { ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack', Engine: 'Microsoft.VisualStudio.Code.Engine', PreRelease: 'Microsoft.VisualStudio.Code.PreRelease', + EnabledApiProposals: 'Microsoft.VisualStudio.Code.EnabledApiProposals', LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages', WebExtension: 'Microsoft.VisualStudio.Code.WebExtension', SponsorLink: 'Microsoft.VisualStudio.Code.SponsorLink', @@ -430,6 +431,12 @@ function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean { return values.length > 0 && values[0].value === 'true'; } +function getEnabledApiProposals(version: IRawGalleryExtensionVersion): string[] { + const values = version.properties ? version.properties.filter(p => p.key === PropertyType.EnabledApiProposals) : []; + const value = (values.length > 0 && values[0].value) || ''; + return value ? value.split(',') : []; +} + function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] { const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : []; const value = (values.length > 0 && values[0].value) || ''; @@ -548,6 +555,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller dependencies: getExtensions(version, PropertyType.Dependency), extensionPack: getExtensions(version, PropertyType.ExtensionPack), engine: getEngine(version), + enabledApiProposals: getEnabledApiProposals(version), localizedLanguages: getLocalizedLanguages(version), targetPlatform: getTargetPlatformForExtensionVersion(version), isPreReleaseVersion: isPreReleaseVersion(version) @@ -579,6 +587,7 @@ interface IRawExtensionsControlManifest { additionalInfo?: string; }>; search?: ISearchPrefferedResults[]; + extensionsEnabledWithPreRelease?: string[]; } abstract class AbstractExtensionGalleryService implements IExtensionGalleryService { @@ -590,6 +599,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi private readonly extensionsControlUrl: string | undefined; private readonly commonHeadersPromise: Promise>; + private readonly extensionsEnabledWithApiProposalVersion: string[]; constructor( storageService: IStorageService | undefined, @@ -606,6 +616,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.extensionsGalleryUrl = isPPEEnabled ? config.servicePPEUrl : config?.serviceUrl; this.extensionsGallerySearchUrl = isPPEEnabled ? undefined : config?.searchUrl; this.extensionsControlUrl = config?.controlUrl; + this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? []; this.commonHeadersPromise = resolveMarketplaceHeaders( productService.version, productService, @@ -704,7 +715,26 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } engine = manifest.engines.vscode; } - return isEngineValid(engine, productVersion.version, productVersion.date); + + if (!isEngineValid(engine, productVersion.version, productVersion.date)) { + return false; + } + + if (!this.areApiProposalsCompatible(extension.identifier, extension.properties.enabledApiProposals)) { + return false; + } + + return true; + } + + private areApiProposalsCompatible(extensionIdentifier: IExtensionIdentifier, enabledApiProposals: string[] | undefined): boolean { + if (!enabledApiProposals) { + return true; + } + if (!this.extensionsEnabledWithApiProposalVersion.includes(extensionIdentifier.id.toLowerCase())) { + return true; + } + return areApiProposalsCompatible(enabledApiProposals); } private async isValidVersion(extension: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { @@ -915,7 +945,18 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi continue; } // Allow any version if includePreRelease flag is set otherwise only release versions are allowed - if (await this.isValidVersion(getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, criteria.productVersion)) { + if (await this.isValidVersion( + extensionIdentifier.id, + rawGalleryExtensionVersion, + includePreRelease ? 'any' : 'release', + criteria.compatible, + allTargetPlatforms, + criteria.targetPlatform, + criteria.productVersion) + ) { + if (criteria.compatible && !this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(rawGalleryExtensionVersion))) { + return null; + } return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext); } if (version && rawGalleryExtensionVersion.version === version) { @@ -957,7 +998,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi }; const stopWatch = new StopWatch(); - let context: IRequestContext | undefined, error: ExtensionGalleryError | undefined, total: number = 0; + let context: IRequestContext | undefined, errorCode: ExtensionGalleryErrorCode | undefined, total: number = 0; try { context = await this.requestService.request({ @@ -989,9 +1030,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return { galleryExtensions: [], total }; } catch (e) { - const errorCode = isCancellationError(e) ? ExtensionGalleryErrorCode.Cancelled : getErrorMessage(e).startsWith('XHR timeout') ? ExtensionGalleryErrorCode.Timeout : ExtensionGalleryErrorCode.Failed; - error = new ExtensionGalleryError(getErrorMessage(e), errorCode); - throw error; + if (isCancellationError(e)) { + errorCode = ExtensionGalleryErrorCode.Cancelled; + throw e; + } else { + const errorMessage = getErrorMessage(e); + errorCode = errorMessage.startsWith('XHR timeout') ? ExtensionGalleryErrorCode.Timeout : ExtensionGalleryErrorCode.Failed; + throw new ExtensionGalleryError(errorMessage, errorCode); + } } finally { this.telemetryService.publicLog2('galleryService:query', { ...query.telemetryData, @@ -1000,7 +1046,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi success: !!context && isSuccess(context), responseBodySize: context?.res.headers['Content-Length'], statusCode: context ? String(context.res.statusCode) : undefined, - errorCode: error?.code, + errorCode, count: String(total) }); } @@ -1132,15 +1178,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return ''; } - async getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async getAllCompatibleVersions(extensionIdentifier: IExtensionIdentifier, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { let query = new Query() .withFlags(Flags.IncludeVersions, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties) .withPage(1, 1); - if (extension.identifier.uuid) { - query = query.withFilter(FilterType.ExtensionId, extension.identifier.uuid); + if (extensionIdentifier.uuid) { + query = query.withFilter(FilterType.ExtensionId, extensionIdentifier.uuid); } else { - query = query.withFilter(FilterType.ExtensionName, extension.identifier.id); + query = query.withFilter(FilterType.ExtensionName, extensionIdentifier.id); } const { galleryExtensions } = await this.queryRawGalleryExtensions(query, CancellationToken.None); @@ -1156,7 +1202,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const validVersions: IRawGalleryExtensionVersion[] = []; await Promise.all(galleryExtensions[0].versions.map(async (version) => { try { - if (await this.isValidVersion(extension.identifier.id, version, includePreRelease ? 'any' : 'release', true, allTargetPlatforms, targetPlatform)) { + if ( + (await this.isValidVersion( + extensionIdentifier.id, + version, includePreRelease ? 'any' : 'release', + true, + allTargetPlatforms, + targetPlatform)) + && this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(version)) + ) { validVersions.push(version); } } catch (error) { /* Ignore error and skip version */ } @@ -1257,6 +1311,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const malicious: IExtensionIdentifier[] = []; const deprecated: IStringDictionary = {}; const search: ISearchPrefferedResults[] = []; + const extensionsEnabledWithPreRelease: string[] = []; if (result) { for (const id of result.malicious) { malicious.push({ id }); @@ -1288,9 +1343,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi search.push(s); } } + if (Array.isArray(result.extensionsEnabledWithPreRelease)) { + for (const id of result.extensionsEnabledWithPreRelease) { + extensionsEnabledWithPreRelease.push(id.toLowerCase()); + } + } } - return { malicious, deprecated, search }; + return { malicious, deprecated, search, extensionsEnabledWithPreRelease }; } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 183f2582871..5786b57ea07 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -159,6 +159,7 @@ export interface IGalleryExtensionProperties { dependencies?: string[]; extensionPack?: string[]; engine?: string; + enabledApiProposals?: string[]; localizedLanguages?: string[]; targetPlatform: TargetPlatform; isPreReleaseVersion: boolean; @@ -326,6 +327,7 @@ export interface IExtensionsControlManifest { readonly malicious: IExtensionIdentifier[]; readonly deprecated: IStringDictionary; readonly search: ISearchPrefferedResults[]; + readonly extensionsEnabledWithPreRelease?: string[]; } export const enum InstallOperation { @@ -367,7 +369,7 @@ export interface IExtensionGalleryService { getExtensions(extensionInfos: ReadonlyArray, options: IExtensionQueryOptions, token: CancellationToken): Promise; isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; - getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; + getAllCompatibleVersions(extensionIdentifier: IExtensionIdentifier, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise; downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise; reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise; @@ -381,7 +383,7 @@ export interface IExtensionGalleryService { export interface InstallExtensionEvent { readonly identifier: IExtensionIdentifier; readonly source: URI | IGalleryExtension; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } @@ -393,14 +395,14 @@ export interface InstallExtensionResult { readonly local?: ILocalExtension; readonly error?: Error; readonly context?: IStringDictionary; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } export interface UninstallExtensionEvent { readonly identifier: IExtensionIdentifier; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } @@ -408,11 +410,16 @@ export interface UninstallExtensionEvent { export interface DidUninstallExtensionEvent { readonly identifier: IExtensionIdentifier; readonly error?: string; - readonly profileLocation?: URI; + readonly profileLocation: URI; readonly applicationScoped?: boolean; readonly workspaceScoped?: boolean; } +export interface DidUpdateExtensionMetadata { + readonly profileLocation: URI; + readonly local: ILocalExtension; +} + export const enum ExtensionGalleryErrorCode { Timeout = 'Timeout', Cancelled = 'Cancelled', @@ -432,12 +439,14 @@ export const enum ExtensionManagementErrorCode { Deprecated = 'Deprecated', Malicious = 'Malicious', Incompatible = 'Incompatible', + IncompatibleApi = 'IncompatibleApi', IncompatibleTargetPlatform = 'IncompatibleTargetPlatform', ReleaseVersionNotFound = 'ReleaseVersionNotFound', Invalid = 'Invalid', Download = 'Download', DownloadSignature = 'DownloadSignature', DownloadFailedWriting = ExtensionGalleryErrorCode.DownloadFailedWriting, + UpdateExistingMetadata = 'UpdateExistingMetadata', UpdateMetadata = 'UpdateMetadata', Extract = 'Extract', Scanning = 'Scanning', @@ -487,7 +496,14 @@ export type InstallOptions = { */ context?: IStringDictionary; }; -export type UninstallOptions = { readonly donotIncludePack?: boolean; readonly donotCheckDependents?: boolean; readonly versionOnly?: boolean; readonly remove?: boolean; readonly profileLocation?: URI }; + +export type UninstallOptions = { + readonly profileLocation?: URI; + readonly donotIncludePack?: boolean; + readonly donotCheckDependents?: boolean; + readonly versionOnly?: boolean; + readonly remove?: boolean; +}; export interface IExtensionManagementParticipant { postInstall(local: ILocalExtension, source: URI | IGalleryExtension, options: InstallOptions, token: CancellationToken): Promise; @@ -504,7 +520,7 @@ export interface IExtensionManagementService { onDidInstallExtensions: Event; onUninstallExtension: Event; onDidUninstallExtension: Event; - onDidUpdateExtensionMetadata: Event; + onDidUpdateExtensionMetadata: Event; zip(extension: ILocalExtension): Promise; unzip(zipLocation: URI): Promise; @@ -521,7 +537,8 @@ export interface IExtensionManagementService { getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; getExtensionsControlManifest(): Promise; copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; - updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; + updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise; + resetPinnedStateForAllUserExtensions(pinned: boolean): Promise; download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts index 9241f4170ab..3506d94d5c9 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts @@ -146,7 +146,7 @@ export class ExtensionManagementCLI { if (areSameExtensions(oldVersion.identifier, newVersion.identifier) && gt(newVersion.version, oldVersion.manifest.version)) { extensionsToUpdate.push({ extension: newVersion, - options: { operation: InstallOperation.Update, installPreReleaseVersion: oldVersion.preRelease, profileLocation } + options: { operation: InstallOperation.Update, installPreReleaseVersion: oldVersion.preRelease, profileLocation, isApplicationScoped: oldVersion.isApplicationScoped } }); } } @@ -224,7 +224,7 @@ export class ExtensionManagementCLI { } extensionsToInstall.push({ extension: gallery, - options: { ...installOptions, installGivenVersion: !!version }, + options: { ...installOptions, installGivenVersion: !!version, isApplicationScoped: installedExtension?.isApplicationScoped }, }); })); @@ -253,7 +253,7 @@ export class ExtensionManagementCLI { const valid = await this.validateVSIX(manifest, force, installOptions.profileLocation, installedExtensions); if (valid) { try { - await this.extensionManagementService.install(vsix, installOptions); + await this.extensionManagementService.install(vsix, { ...installOptions, installGivenVersion: true }); this.logger.info(localize('successVsixInstall', "Extension '{0}' was successfully installed.", basename(vsix))); } catch (error) { if (isCancellationError(error)) { diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 6c3e289db7d..65e8735bd10 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion, DidUpdateExtensionMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; @@ -43,7 +43,7 @@ export class ExtensionManagementChannel implements IServerChannel { onDidInstallExtensions: Event; onUninstallExtension: Event; onDidUninstallExtension: Event; - onDidUpdateExtensionMetadata: Event; + onDidUpdateExtensionMetadata: Event; constructor(private service: IExtensionManagementService, private getUriTransformer: (requestContext: any) => IURITransformer | null) { this.onInstallExtension = Event.buffer(service.onInstallExtension, true); @@ -89,7 +89,12 @@ export class ExtensionManagementChannel implements IServerChannel { }); } case 'onDidUpdateExtensionMetadata': { - return Event.map(this.onDidUpdateExtensionMetadata, e => transformOutgoingExtension(e, uriTransformer)); + return Event.map(this.onDidUpdateExtensionMetadata, e => { + return { + local: transformOutgoingExtension(e.local, uriTransformer), + profileLocation: transformOutgoingURI(e.profileLocation, uriTransformer) + }; + }); } } @@ -153,6 +158,9 @@ export class ExtensionManagementChannel implements IServerChannel { const e = await this.service.updateMetadata(transformIncomingExtension(args[0], uriTransformer), args[1], transformIncomingURI(args[2], uriTransformer)); return transformOutgoingExtension(e, uriTransformer); } + case 'resetPinnedStateForAllUserExtensions': { + return this.service.resetPinnedStateForAllUserExtensions(args[0]); + } case 'getExtensionsControlManifest': { return this.service.getExtensionsControlManifest(); } @@ -168,7 +176,11 @@ export class ExtensionManagementChannel implements IServerChannel { } } -export type ExtensionEventResult = InstallExtensionEvent | InstallExtensionResult | UninstallExtensionEvent | DidUninstallExtensionEvent; +export interface ExtensionEventResult { + readonly profileLocation: URI; + readonly local?: ILocalExtension; + readonly applicationScoped?: boolean; +} export class ExtensionManagementChannelClient extends Disposable implements IExtensionManagementService { @@ -186,7 +198,7 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt private readonly _onDidUninstallExtension = this._register(new Emitter()); get onDidUninstallExtension() { return this._onDidUninstallExtension.event; } - private readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); + private readonly _onDidUpdateExtensionMetadata = this._register(new Emitter()); get onDidUpdateExtensionMetadata() { return this._onDidUpdateExtensionMetadata.event; } constructor(private readonly channel: IChannel) { @@ -195,16 +207,12 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt this._register(this.channel.listen('onDidInstallExtensions')(results => this.fireEvent(this._onDidInstallExtensions, results.map(e => ({ ...e, local: e.local ? transformIncomingExtension(e.local, null) : e.local, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) }))))); this._register(this.channel.listen('onUninstallExtension')(e => this.fireEvent(this._onUninstallExtension, { ...e, profileLocation: URI.revive(e.profileLocation) }))); this._register(this.channel.listen('onDidUninstallExtension')(e => this.fireEvent(this._onDidUninstallExtension, { ...e, profileLocation: URI.revive(e.profileLocation) }))); - this._register(this.channel.listen('onDidUpdateExtensionMetadata')(e => this._onDidUpdateExtensionMetadata.fire(transformIncomingExtension(e, null)))); + this._register(this.channel.listen('onDidUpdateExtensionMetadata')(e => this.fireEvent(this._onDidUpdateExtensionMetadata, { profileLocation: URI.revive(e.profileLocation), local: transformIncomingExtension(e.local, null) }))); } - protected fireEvent(event: Emitter, data: InstallExtensionEvent): void; - protected fireEvent(event: Emitter, data: InstallExtensionResult[]): void; - protected fireEvent(event: Emitter, data: UninstallExtensionEvent): void; - protected fireEvent(event: Emitter, data: DidUninstallExtensionEvent): void; - protected fireEvent(event: Emitter, data: ExtensionEventResult): void; - protected fireEvent(event: Emitter, data: ExtensionEventResult[]): void; - protected fireEvent(event: Emitter, data: E): void { + protected fireEvent(event: Emitter, data: E): void; + protected fireEvent(event: Emitter, data: E[]): void; + protected fireEvent(event: Emitter, data: E | E[]): void { event.fire(data); } @@ -284,6 +292,10 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt .then(extension => transformIncomingExtension(extension, null)); } + resetPinnedStateForAllUserExtensions(pinned: boolean): Promise { + return this.channel.call('resetPinnedStateForAllUserExtensions', [pinned]); + } + toggleAppliationScope(local: ILocalExtension, fromProfileLocation: URI): Promise { return this.channel.call('toggleAppliationScope', [local, fromProfileLocation]) .then(extension => transformIncomingExtension(extension, null)); diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index e92897f174f..57831802fe4 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -24,7 +24,7 @@ import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductVersion, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap, parseEnabledApiProposalNames } from 'vs/platform/extensions/common/extensions'; import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -554,14 +554,19 @@ type NlsConfiguration = { class ExtensionsScanner extends Disposable { + private readonly extensionsEnabledWithApiProposalVersion: string[]; + constructor( private readonly obsoleteFile: URI, @IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @IFileService protected readonly fileService: IFileService, + @IProductService productService: IProductService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, @ILogService protected readonly logService: ILogService ) { super(); + this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? []; } async scanExtensions(input: ExtensionScannerInput): Promise { @@ -653,7 +658,7 @@ class ExtensionsScanner extends Disposable { const type = metadata?.isSystem ? ExtensionType.System : input.type; const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin; manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input)); - const extension: IRelaxedScannedExtension = { + let extension: IRelaxedScannedExtension = { type, identifier, manifest, @@ -665,7 +670,14 @@ class ExtensionsScanner extends Disposable { isValid: true, validations: [] }; - return input.validate ? this.validate(extension, input) : extension; + if (input.validate) { + extension = this.validate(extension, input); + } + if (manifest.enabledApiProposals && (!this.environmentService.isBuilt || this.extensionsEnabledWithApiProposalVersion.includes(id.toLowerCase()))) { + manifest.originalEnabledApiProposals = manifest.enabledApiProposals; + manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]); + } + return extension; } } catch (e) { if (input.type !== ExtensionType.System) { @@ -677,7 +689,8 @@ class ExtensionsScanner extends Disposable { validate(extension: IRelaxedScannedExtension, input: ExtensionScannerInput): IRelaxedScannedExtension { let isValid = true; - const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin); + const validateApiVersion = this.environmentService.isBuilt && this.extensionsEnabledWithApiProposalVersion.includes(extension.identifier.id.toLowerCase()); + const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin, validateApiVersion); for (const [severity, message] of validations) { if (severity === Severity.Error) { isValid = false; @@ -689,7 +702,7 @@ class ExtensionsScanner extends Disposable { return extension; } - async scanExtensionManifest(extensionLocation: URI): Promise { + private async scanExtensionManifest(extensionLocation: URI): Promise { const manifestLocation = joinPath(extensionLocation, 'package.json'); let content; try { @@ -878,9 +891,11 @@ class CachedExtensionsScanner extends ExtensionsScanner { @IExtensionsProfileScannerService extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService uriIdentityService: IUriIdentityService, @IFileService fileService: IFileService, + @IProductService productService: IProductService, + @IEnvironmentService environmentService: IEnvironmentService, @ILogService logService: ILogService ) { - super(obsoleteFile, extensionsProfileScannerService, uriIdentityService, fileService, logService); + super(obsoleteFile, extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService); } override async scanExtensions(input: ExtensionScannerInput): Promise { @@ -888,7 +903,7 @@ class CachedExtensionsScanner extends ExtensionsScanner { const cacheContents = await this.readExtensionCache(cacheFile); this.input = input; if (cacheContents && cacheContents.input && ExtensionScannerInput.equals(cacheContents.input, this.input)) { - this.logService.debug('Using cached extensions scan result', input.location.toString()); + this.logService.debug('Using cached extensions scan result', input.type === ExtensionType.System ? 'system' : 'user', input.location.toString()); this.cacheValidatorThrottler.trigger(() => this.validateCache()); return cacheContents.result.map((extension) => { // revive URI object diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 38757db3a67..60b13367651 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { Promises, Queue } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -12,7 +13,7 @@ import { CancellationError, getErrorMessage } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ResourceSet } from 'vs/base/common/map'; +import { ResourceMap, ResourceSet } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; @@ -186,7 +187,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return extensionsToInstall; } - async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource): Promise { + async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise { this.logService.trace('ExtensionManagementService#updateMetadata', local.identifier.id); if (metadata.isPreReleaseVersion) { metadata.preRelease = true; @@ -204,7 +205,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } local = await this.extensionsScanner.updateMetadata(local, metadata, profileLocation); this.manifestCache.invalidate(profileLocation); - this._onDidUpdateExtensionMetadata.fire(local); + this._onDidUpdateExtensionMetadata.fire({ local, profileLocation }); return local; } @@ -363,7 +364,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi const collectFilesFromDirectory = async (dir: string): Promise => { let entries = await pfs.Promises.readdir(dir); entries = entries.map(e => path.join(dir, e)); - const stats = await Promise.all(entries.map(e => pfs.Promises.stat(e))); + const stats = await Promise.all(entries.map(e => fs.promises.stat(e))); let promise: Promise = Promise.resolve([]); stats.forEach((stat, index) => { const entry = entries[index]; @@ -484,6 +485,19 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } +type UpdateMetadataErrorClassification = { + owner: 'sandy081'; + comment: 'Update metadata error'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension identifier' }; + code?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' }; + isProfile?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Is writing into profile' }; +}; +type UpdateMetadataErrorEvent = { + extensionId: string; + code?: string; + isProfile?: boolean; +}; + export class ExtensionsScanner extends Disposable { private readonly uninstalledResource: URI; @@ -492,12 +506,16 @@ export class ExtensionsScanner extends Disposable { private readonly _onExtract = this._register(new Emitter()); readonly onExtract = this._onExtract.event; + private scanAllExtensionPromise = new ResourceMap>(); + private scanUserExtensionsPromise = new ResourceMap>(); + constructor( private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise, @IFileService private readonly fileService: IFileService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, ) { super(); @@ -515,9 +533,21 @@ export class ExtensionsScanner extends Disposable { const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; let scannedExtensions: IScannedExtension[] = []; if (type === null || type === ExtensionType.System) { - scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); + let scanAllExtensionsPromise = this.scanAllExtensionPromise.get(profileLocation); + if (!scanAllExtensionsPromise) { + scanAllExtensionsPromise = this.extensionsScannerService.scanAllExtensions({ includeInvalid: true, useCache: true }, userScanOptions, false) + .finally(() => this.scanAllExtensionPromise.delete(profileLocation)); + this.scanAllExtensionPromise.set(profileLocation, scanAllExtensionsPromise); + } + scannedExtensions.push(...await scanAllExtensionsPromise); } else if (type === ExtensionType.User) { - scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(userScanOptions)); + let scanUserExtensionsPromise = this.scanUserExtensionsPromise.get(profileLocation); + if (!scanUserExtensionsPromise) { + scanUserExtensionsPromise = this.extensionsScannerService.scanUserExtensions(userScanOptions) + .finally(() => this.scanUserExtensionsPromise.delete(profileLocation)); + this.scanUserExtensionsPromise.set(profileLocation, scanUserExtensionsPromise); + } + scannedExtensions.push(...await scanUserExtensionsPromise); } scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); @@ -567,7 +597,8 @@ export class ExtensionsScanner extends Disposable { try { await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); + this.telemetryService.publicLog2('extension:extract', { extensionId: extensionKey.id, code: `${toFileOperationResult(error)}` }); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateExistingMetadata); } } else { try { @@ -587,6 +618,7 @@ export class ExtensionsScanner extends Disposable { try { await this.extensionsScannerService.updateMetadata(tempLocation, metadata); } catch (error) { + this.telemetryService.publicLog2('extension:extract', { extensionId: extensionKey.id, code: `${toFileOperationResult(error)}` }); throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); } @@ -602,6 +634,7 @@ export class ExtensionsScanner extends Disposable { } catch (error) { if (error.code === 'ENOTEMPTY') { this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id); + try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ } } else { this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempLocation); throw error; @@ -641,6 +674,7 @@ export class ExtensionsScanner extends Disposable { await this.extensionsScannerService.updateMetadata(local.location, metadata); } } catch (error) { + this.telemetryService.publicLog2('extension:extract', { extensionId: local.identifier.id, code: `${toFileOperationResult(error)}`, isProfile: !!profileLocation }); throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); } return this.scanLocalExtension(local.location, local.type, profileLocation); @@ -758,7 +792,7 @@ export class ExtensionsScanner extends Disposable { } } - private async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise { + async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise { try { if (profileLocation) { const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); @@ -911,13 +945,9 @@ class InstallExtensionInProfileTask extends AbstractExtensionTask { - const installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); - return installed.find(i => areSameExtensions(i.identifier, this.identifier)); - } - protected async doRun(token: CancellationToken): Promise { - const existingExtension = await this.getExistingExtension(); + const installed = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation, this.options.productVersion); + const existingExtension = installed.find(i => areSameExtensions(i.identifier, this.identifier)); if (existingExtension) { this._operation = InstallOperation.Update; } @@ -932,6 +962,8 @@ class InstallExtensionInProfileTask extends AbstractExtensionTask; } -export const enum ExtensionSignatureVerificationCode { - 'None' = 'None', +export enum ExtensionSignatureVerificationCode { + 'Success' = 'Success', 'RequiredArgumentMissing' = 'RequiredArgumentMissing', 'InvalidArgument' = 'InvalidArgument', 'PackageIsUnreadable' = 'PackageIsUnreadable', @@ -51,7 +51,6 @@ export const enum ExtensionSignatureVerificationCode { 'SignatureArchiveIsInvalidZip' = 'SignatureArchiveIsInvalidZip', 'SignatureArchiveHasSameSignatureFile' = 'SignatureArchiveHasSameSignatureFile', - 'Success' = 'Success', 'PackageIntegrityCheckFailed' = 'PackageIntegrityCheckFailed', 'SignatureIsInvalid' = 'SignatureIsInvalid', 'SignatureManifestIsInvalid' = 'SignatureManifestIsInvalid', diff --git a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index ba8e026b893..a56087b0b51 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getErrorMessage } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { combinedDisposable, Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import { ResourceSet } from 'vs/base/common/map'; @@ -40,7 +41,7 @@ export class ExtensionsWatcher extends Disposable { private readonly logService: ILogService, ) { super(); - this.initialize().then(null, error => logService.error(error)); + this.initialize().then(null, error => logService.error('Error while initializing Extensions Watcher', getErrorMessage(error))); } private async initialize(): Promise { diff --git a/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts b/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts index ce93c6e73d7..178293d8874 100644 --- a/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts +++ b/src/vs/platform/extensionManagement/test/common/configRemotes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getDomainsOfRemotes, getRemotes } from 'vs/platform/extensionManagement/common/configRemotes'; diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts index cebafab4714..a4c9afd9c58 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { isUUID } from 'vs/base/common/uuid'; diff --git a/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts b/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts index d0813771c18..1c3d62f7f0b 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; diff --git a/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts b/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts index a3c2603a1eb..64aad4fbd41 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { deepClone } from 'vs/base/common/objects'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILocalizedString } from 'vs/platform/action/common/action'; +import { IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; -import { IExtensionManifest, IConfiguration } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { NullLogger } from 'vs/platform/log/common/log'; const manifest: IExtensionManifest = { @@ -62,7 +63,7 @@ suite('Localize Manifest', () => { assert.strictEqual(localizedManifest.contributes?.commands?.[0].title, 'Test Command'); assert.strictEqual(localizedManifest.contributes?.commands?.[0].category, 'Test Category'); assert.strictEqual(localizedManifest.contributes?.authentication?.[0].label, 'Test Authentication'); - assert.strictEqual((localizedManifest.contributes?.configuration as IConfiguration).title, 'Test Configuration'); + assert.strictEqual((localizedManifest.contributes?.configuration as IConfigurationNode).title, 'Test Configuration'); }); test('replaces template strings with fallback if not found in translations', function () { @@ -81,7 +82,7 @@ suite('Localize Manifest', () => { assert.strictEqual(localizedManifest.contributes?.commands?.[0].title, 'Test Command'); assert.strictEqual(localizedManifest.contributes?.commands?.[0].category, 'Test Category'); assert.strictEqual(localizedManifest.contributes?.authentication?.[0].label, 'Test Authentication'); - assert.strictEqual((localizedManifest.contributes?.configuration as IConfiguration).title, 'Test Configuration'); + assert.strictEqual((localizedManifest.contributes?.configuration as IConfigurationNode).title, 'Test Configuration'); }); test('replaces template strings - command title & categories become ILocalizedString', function () { @@ -111,7 +112,7 @@ suite('Localize Manifest', () => { // Everything else stays as a string. assert.strictEqual(localizedManifest.contributes?.authentication?.[0].label, 'Testauthentifizierung'); - assert.strictEqual((localizedManifest.contributes?.configuration as IConfiguration).title, 'Testkonfiguration'); + assert.strictEqual((localizedManifest.contributes?.configuration as IConfigurationNode).title, 'Testkonfiguration'); }); test('replaces template strings - is best effort #164630', function () { diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts index 5e71b85714e..d94305a83fc 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { VSBuffer } from 'vs/base/common/buffer'; import { joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts index e803de72c79..bae93bde803 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index 9e72c1c174a..8c0569fe67b 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { dirname, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index cee5eaeedef..5fae8b4b1e2 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -8,7 +8,8 @@ import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import * as semver from 'vs/base/common/semver/semver'; -import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, parseApiProposals } from 'vs/platform/extensions/common/extensions'; +import { allApiProposals } from 'vs/platform/extensions/common/extensionsApiProposals'; export interface IParsedVersion { hasCaret: boolean; @@ -239,7 +240,7 @@ export function isValidVersion(_inputVersion: string | INormalizedVersion, _inpu type ProductDate = string | Date | undefined; -export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean): readonly [Severity, string][] { +export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, validateApiVersion: boolean): readonly [Severity, string][] { const validations: [Severity, string][] = []; if (typeof extensionManifest.publisher !== 'undefined' && typeof extensionManifest.publisher !== 'string') { validations.push([Severity.Error, nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")]); @@ -314,12 +315,22 @@ export function validateExtensionManifest(productVersion: string, productDate: P } const notices: string[] = []; - const isValid = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); - if (!isValid) { + const validExtensionVersion = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); + if (!validExtensionVersion) { for (const notice of notices) { validations.push([Severity.Error, notice]); } } + + if (validateApiVersion && extensionManifest.enabledApiProposals?.length) { + const incompatibleNotices: string[] = []; + if (!areApiProposalsCompatible([...extensionManifest.enabledApiProposals], incompatibleNotices)) { + for (const notice of incompatibleNotices) { + validations.push([Severity.Error, notice]); + } + } + } + return validations; } @@ -338,6 +349,34 @@ export function isEngineValid(engine: string, version: string, date: ProductDate return engine === '*' || isVersionValid(version, date, engine); } +export function areApiProposalsCompatible(apiProposals: string[]): boolean; +export function areApiProposalsCompatible(apiProposals: string[], notices: string[]): boolean; +export function areApiProposalsCompatible(apiProposals: string[], productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>): boolean; +export function areApiProposalsCompatible(apiProposals: string[], arg1?: any): boolean { + if (apiProposals.length === 0) { + return true; + } + const notices: string[] | undefined = Array.isArray(arg1) ? arg1 : undefined; + const productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }> = (notices ? undefined : arg1) ?? allApiProposals; + const incompatibleNotices: string[] = []; + const parsedProposals = parseApiProposals(apiProposals); + for (const { proposalName, version } of parsedProposals) { + const existingProposal = productApiProposals[proposalName]; + if (!existingProposal) { + continue; + } + if (!version) { + continue; + } + if (existingProposal.version !== version) { + incompatibleNotices.push(nls.localize('apiProposalMismatch', "Extension is using an API proposal '{0}' that is not compatible with the current version of VS Code.", proposalName)); + } + } + notices?.push(...incompatibleNotices); + return incompatibleNotices.length === 0; + +} + function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean { const desiredVersion = normalizeVersion(parseVersion(requestedVersion)); diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index cfe5313b905..822260bdc2f 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -21,19 +21,6 @@ export interface ICommand { category?: string | ILocalizedString; } -export interface IConfigurationProperty { - description: string; - type: string | string[]; - default?: any; -} - -export interface IConfiguration { - id?: string; - order?: number; - title?: string; - properties: { [key: string]: IConfigurationProperty }; -} - export interface IDebugger { label?: string; type: string; @@ -41,7 +28,7 @@ export interface IDebugger { } export interface IGrammar { - language: string; + language?: string; } export interface IJSONValidation { @@ -182,7 +169,7 @@ export interface ILocalizationContribution { export interface IExtensionContributions { commands?: ICommand[]; - configuration?: IConfiguration | IConfiguration[]; + configuration?: any; debuggers?: IDebugger[]; grammars?: IGrammar[]; jsonValidation?: IJSONValidation[]; @@ -239,7 +226,9 @@ export interface IExtensionIdentifier { } export const EXTENSION_CATEGORIES = [ + 'AI', 'Azure', + 'Chat', 'Data Science', 'Debuggers', 'Extension Packs', @@ -256,8 +245,6 @@ export const EXTENSION_CATEGORIES = [ 'Testing', 'Themes', 'Visualization', - 'AI', - 'Chat', 'Other', ]; @@ -284,6 +271,7 @@ export interface IRelaxedExtensionManifest { contributes?: IExtensionContributions; repository?: { url: string }; bugs?: { url: string }; + originalEnabledApiProposals?: readonly string[]; enabledApiProposals?: readonly string[]; api?: string; scripts?: { [key: string]: string }; @@ -492,6 +480,17 @@ export function isResolverExtension(manifest: IExtensionManifest, remoteAuthorit return false; } +export function parseApiProposals(enabledApiProposals: string[]): { proposalName: string; version?: number }[] { + return enabledApiProposals.map(proposal => { + const [proposalName, version] = proposal.split('@'); + return { proposalName, version: version ? parseInt(version) : undefined }; + }); +} + +export function parseEnabledApiProposalNames(enabledApiProposals: string[]): string[] { + return enabledApiProposals.map(proposal => proposal.split('@')[0]); +} + export const IBuiltinExtensionsScannerService = createDecorator('IBuiltinExtensionsScannerService'); export interface IBuiltinExtensionsScannerService { readonly _serviceBrand: undefined; diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts new file mode 100644 index 00000000000..a3f677336d1 --- /dev/null +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -0,0 +1,376 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY. + +const _allApiProposals = { + activeComment: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.activeComment.d.ts', + }, + aiRelatedInformation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', + }, + aiTextSearchProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', + }, + attributableCoverage: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts', + }, + authGetSessions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', + }, + authLearnMore: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', + }, + authSession: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', + }, + canonicalUriProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', + }, + chatParticipantAdditions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts', + }, + chatParticipantPrivate: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', + version: 2 + }, + chatProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', + }, + chatTab: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts', + }, + chatVariableResolver: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts', + }, + codeActionAI: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionAI.d.ts', + }, + codeActionRanges: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionRanges.d.ts', + }, + codiconDecoration: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', + }, + commentReactor: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReactor.d.ts', + }, + commentReveal: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReveal.d.ts', + }, + commentThreadApplicability: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts', + }, + commentingRangeHint: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentingRangeHint.d.ts', + }, + commentsDraftState: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsDraftState.d.ts', + }, + contribAccessibilityHelpContent: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribAccessibilityHelpContent.d.ts', + }, + contribCommentEditorActionsMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', + }, + contribCommentPeekContext: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts', + }, + contribCommentThreadAdditionalMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts', + }, + contribCommentsViewThreadMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts', + }, + contribDiffEditorGutterToolBarMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribDiffEditorGutterToolBarMenus.d.ts', + }, + contribEditSessions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts', + }, + contribEditorContentMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts', + }, + contribIssueReporter: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribIssueReporter.d.ts', + }, + contribLabelFormatterWorkspaceTooltip: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', + }, + contribMenuBarHome: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMenuBarHome.d.ts', + }, + contribMergeEditorMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMergeEditorMenus.d.ts', + }, + contribMultiDiffEditorMenus: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMultiDiffEditorMenus.d.ts', + }, + contribNotebookStaticPreloads: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribNotebookStaticPreloads.d.ts', + }, + contribRemoteHelp: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts', + }, + contribShareMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribShareMenu.d.ts', + }, + contribSourceControlHistoryItemGroupMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemGroupMenu.d.ts', + }, + contribSourceControlHistoryItemMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemMenu.d.ts', + }, + contribSourceControlInputBoxMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlInputBoxMenu.d.ts', + }, + contribSourceControlTitleMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlTitleMenu.d.ts', + }, + contribStatusBarItems: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribStatusBarItems.d.ts', + }, + contribViewsRemote: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', + }, + contribViewsWelcome: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', + }, + createFileSystemWatcher: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts', + }, + customEditorMove: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', + }, + debugVisualization: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugVisualization.d.ts', + }, + defaultChatParticipant: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts', + }, + diffCommand: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', + }, + diffContentOptions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffContentOptions.d.ts', + }, + documentFiltersExclusive: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', + }, + documentPaste: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentPaste.d.ts', + }, + editSessionIdentityProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts', + }, + editorHoverVerbosityLevel: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts', + }, + editorInsets: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', + }, + embeddings: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.embeddings.d.ts', + }, + extensionRuntime: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', + }, + extensionsAny: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', + }, + externalUriOpener: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts', + }, + fileComments: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileComments.d.ts', + }, + fileSearchProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts', + }, + findFiles2: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findFiles2.d.ts', + }, + findTextInFiles: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', + }, + fsChunks: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts', + }, + idToken: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.idToken.d.ts', + }, + inlineCompletionsAdditions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts', + }, + inlineEdit: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineEdit.d.ts', + }, + interactive: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactive.d.ts', + }, + interactiveWindow: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts', + }, + ipc: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', + }, + languageModelSystem: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts', + }, + languageStatusText: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageStatusText.d.ts', + }, + lmTools: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.lmTools.d.ts', + version: 2 + }, + mappedEditsProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', + }, + multiDocumentHighlightProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts', + }, + newSymbolNamesProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.newSymbolNamesProvider.d.ts', + }, + notebookCellExecution: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecution.d.ts', + }, + notebookCellExecutionState: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', + }, + notebookControllerAffinityHidden: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts', + }, + notebookDeprecated: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', + }, + notebookExecution: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookExecution.d.ts', + }, + notebookKernelSource: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts', + }, + notebookLiveShare: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts', + }, + notebookMessaging: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts', + }, + notebookMime: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts', + }, + notebookVariableProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts', + }, + portsAttributes: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', + }, + profileContentHandlers: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts', + }, + quickDiffProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts', + }, + quickPickItemTooltip: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts', + }, + quickPickSortByLabel: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', + }, + resolvers: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts', + }, + scmActionButton: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', + }, + scmHistoryProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts', + }, + scmMultiDiffEditor: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts', + }, + scmSelectedProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', + }, + scmTextDocument: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmTextDocument.d.ts', + }, + scmValidation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', + }, + shareProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.shareProvider.d.ts', + }, + showLocal: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.showLocal.d.ts', + }, + speech: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.speech.d.ts', + }, + tabInputMultiDiff: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts', + }, + tabInputTextMerge: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts', + }, + taskPresentationGroup: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', + }, + telemetry: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', + }, + terminalDataWriteEvent: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', + }, + terminalDimensions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', + }, + terminalExecuteCommandEvent: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts', + }, + terminalQuickFixProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', + }, + terminalSelection: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', + }, + terminalShellIntegration: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts', + }, + testObserver: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', + }, + textSearchProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', + }, + timeline: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', + }, + tokenInformation: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', + }, + treeViewActiveItem: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts', + }, + treeViewMarkdownMessage: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewMarkdownMessage.d.ts', + }, + treeViewReveal: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewReveal.d.ts', + }, + tunnelFactory: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts', + }, + tunnels: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnels.d.ts', + }, + workspaceTrust: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts', + } +}; +export const allApiProposals = Object.freeze<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>(_allApiProposals); +export type ApiProposalName = keyof typeof _allApiProposals; diff --git a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts index fec3b6eded6..106963ac0b5 100644 --- a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts +++ b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { canceled } from 'vs/base/common/errors'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; -import { Event } from 'vs/base/common/event'; -import { ILogService } from 'vs/platform/log/common/log'; -import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { Promises } from 'vs/base/common/async'; +import { canceled } from 'vs/base/common/errors'; +import { Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; +import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WindowUtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter { +export class ExtensionHostStarter extends Disposable implements IDisposable, IExtensionHostStarter { readonly _serviceBrand: undefined; @@ -29,16 +29,18 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter @IWindowsMainService private readonly _windowsMainService: IWindowsMainService, @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { + super(); // On shutdown: gracefully await extension host shutdowns - this._lifecycleMainService.onWillShutdown(e => { + this._register(this._lifecycleMainService.onWillShutdown(e => { this._shutdown = true; e.join('extHostStarter', this._waitForAllExit(6000)); - }); + })); } - dispose(): void { + override dispose(): void { // Intentionally not killing the extension host processes + super.dispose(); } private _getExtHost(id: string): WindowUtilityProcess { @@ -72,7 +74,8 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter const id = String(++ExtensionHostStarter._lastId); const extHost = new WindowUtilityProcess(this._logService, this._windowsMainService, this._telemetryService, this._lifecycleMainService); this._extHosts.set(id, extHost); - extHost.onExit(({ pid, code, signal }) => { + const disposable = extHost.onExit(({ pid, code, signal }) => { + disposable.dispose(); this._logService.info(`Extension host with pid ${pid} exited with code: ${code}, signal: ${signal}.`); setTimeout(() => { extHost.dispose(); diff --git a/src/vs/platform/extensions/test/common/extensionValidator.test.ts b/src/vs/platform/extensions/test/common/extensionValidator.test.ts index 9885ec4e29a..6ac5821e08a 100644 --- a/src/vs/platform/extensions/test/common/extensionValidator.test.ts +++ b/src/vs/platform/extensions/test/common/extensionValidator.test.ts @@ -2,11 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { INormalizedVersion, IParsedVersion, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; +import { areApiProposalsCompatible, INormalizedVersion, IParsedVersion, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; suite('Extension Version Validator', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + const productVersion = '2021-05-11T21:54:30.577Z'; test('isValidVersionStr', () => { @@ -419,4 +423,21 @@ suite('Extension Version Validator', () => { }; assert.strictEqual(isValidExtensionVersion('1.44.0', undefined, manifest, false, []), false); }); + + test('areApiProposalsCompatible', () => { + assert.strictEqual(areApiProposalsCompatible([]), true); + assert.strictEqual(areApiProposalsCompatible([], ['hello']), true); + assert.strictEqual(areApiProposalsCompatible([], {}), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], {}), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal1': { proposal: '' } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal1': { proposal: '', version: 1 } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '', version: 1 } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal2': { proposal: '' } }), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1', 'proposal2'], {}), true); + assert.strictEqual(areApiProposalsCompatible(['proposal1', 'proposal2'], { 'proposal1': { proposal: '' } }), true); + + assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '', version: 2 } }), false); + assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '' } }), false); + }); + }); diff --git a/src/vs/platform/extensions/test/common/extensions.test.ts b/src/vs/platform/extensions/test/common/extensions.test.ts new file mode 100644 index 00000000000..7b81268b347 --- /dev/null +++ b/src/vs/platform/extensions/test/common/extensions.test.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { parseEnabledApiProposalNames } from 'vs/platform/extensions/common/extensions'; + +suite('Parsing Enabled Api Proposals', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('parsingEnabledApiProposals', () => { + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@1'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@randomstring'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@1234'])); + assert.deepStrictEqual(['activeComment', 'commentsDraftState'], parseEnabledApiProposalNames(['activeComment', 'commentsDraftState@1234_random'])); + }); + +}); diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 149665bb571..82d4f9ba0f3 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -1460,7 +1460,7 @@ export interface IGlobPatterns { } export interface IFilesConfiguration { - files: IFilesConfigurationNode; + files?: IFilesConfigurationNode; } export interface IFilesConfigurationNode { @@ -1470,6 +1470,7 @@ export interface IFilesConfigurationNode { watcherInclude: string[]; encoding: string; autoGuessEncoding: boolean; + candidateGuessEncodings: string[]; defaultLanguage: string; trimTrailingWhitespace: boolean; autoSave: string; diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index 05ff156de89..eef16ccfcb0 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -181,16 +181,14 @@ export interface IUniversalWatcher extends IWatcher { export abstract class AbstractWatcherClient extends Disposable { - private static readonly MAX_RESTARTS_PER_REQUEST_ERROR = 3; // how often we give a request a chance to restart on error - private static readonly MAX_RESTARTS_PER_UNKNOWN_ERROR = 10; // how often we give the watcher a chance to restart on unknown errors (like crash) + private static readonly MAX_RESTARTS = 5; private watcher: IWatcher | undefined; private readonly watcherDisposables = this._register(new MutableDisposable()); private requests: IWatchRequest[] | undefined = undefined; - private restartsPerRequestError = new Map(); - private restartsPerUnknownError = 0; + private restartCounter = 0; constructor( private readonly onFileChanges: (changes: IFileChange[]) => void, @@ -224,41 +222,51 @@ export abstract class AbstractWatcherClient extends Disposable { protected onError(error: string, failedRequest?: IUniversalWatchRequest): void { - // Restart on error (up to N times, if enabled) - if (this.options.restartOnError && this.requests?.length) { - - // A request failed - if (failedRequest) { - const restartsPerRequestError = this.restartsPerRequestError.get(failedRequest.path) ?? 0; - if (restartsPerRequestError < AbstractWatcherClient.MAX_RESTARTS_PER_REQUEST_ERROR) { - this.error(`restarting watcher from error in watch request (retrying request): ${error} (${JSON.stringify(failedRequest)})`); - this.restartsPerRequestError.set(failedRequest.path, restartsPerRequestError + 1); - this.restart(this.requests); - } else { - this.error(`restarting watcher from error in watch request (skipping request): ${error} (${JSON.stringify(failedRequest)})`); - this.restart(this.requests.filter(request => request.path !== failedRequest.path)); - } - } - - // Any request failed or process crashed - else { - if (this.restartsPerUnknownError < AbstractWatcherClient.MAX_RESTARTS_PER_UNKNOWN_ERROR) { - this.error(`restarting watcher after unknown global error: ${error}`); - this.restartsPerUnknownError++; - this.restart(this.requests); - } else { - this.error(`giving up attempting to restart watcher after error: ${error}`); - } + // Restart on error (up to N times, if possible) + if (this.canRestart(error, failedRequest)) { + if (this.restartCounter < AbstractWatcherClient.MAX_RESTARTS && this.requests) { + this.error(`restarting watcher after unexpected error: ${error}`); + this.restart(this.requests); + } else { + this.error(`gave up attempting to restart watcher after unexpected error: ${error}`); } } - // Do not attempt to restart if not enabled + // Do not attempt to restart otherwise, report the error else { this.error(error); } } + private canRestart(error: string, failedRequest?: IUniversalWatchRequest): boolean { + if (!this.options.restartOnError) { + return false; // disabled by options + } + + if (failedRequest) { + // do not treat a failing request as a reason to restart the entire + // watcher. it is possible that from a large amount of watch requests + // some fail and we would constantly restart all requests only because + // of that. rather, continue the watcher and leave the failed request + return false; + } + + if ( + error.indexOf('No space left on device') !== -1 || + error.indexOf('EMFILE') !== -1 + ) { + // do not restart when the error indicates that the system is running + // out of handles for file watching. this is not recoverable anyway + // and needs changes to the system before continuing + return false; + } + + return true; + } + private restart(requests: IUniversalWatchRequest[]): void { + this.restartCounter++; + this.init(); this.watch(requests); } diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 180aa7e2960..9993c5f82ba 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import { gracefulify } from 'graceful-fs'; +import { Stats, promises } from 'fs'; import { Barrier, retry } from 'vs/base/common/async'; import { ResourceMap } from 'vs/base/common/map'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -24,22 +23,9 @@ import { readFileIntoStream } from 'vs/platform/files/common/io'; import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage } from 'vs/platform/files/common/watcher'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/common/diskFileSystemProvider'; -import { toErrorMessage } from 'vs/base/common/errorMessage'; import { UniversalWatcherClient } from 'vs/platform/files/node/watcher/watcherClient'; import { NodeJSWatcherClient } from 'vs/platform/files/node/watcher/nodejs/nodejsClient'; -/** - * Enable graceful-fs very early from here to have it enabled - * in all contexts that leverage the disk file system provider. - */ -(() => { - try { - gracefulify(fs); - } catch (error) { - console.error(`Error enabling graceful-fs: ${toErrorMessage(error)}`); - } -})(); - export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, @@ -139,7 +125,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple } } - private toType(entry: fs.Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType { + private toType(entry: Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType { // Signal file type by checking for file / directory, except: // - symbolic links pointing to nonexistent files are FileType.Unknown @@ -217,7 +203,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple const filePath = this.toFilePath(resource); - return await Promises.readFile(filePath); + return await promises.readFile(filePath); } catch (error) { throw this.toFileSystemProviderError(error); } finally { @@ -368,7 +354,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple try { const { stat } = await SymlinkSupport.stat(filePath); if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) { - await Promises.chmod(filePath, stat.mode | 0o200); + await promises.chmod(filePath, stat.mode | 0o200); } } catch (error) { if (error.code !== 'ENOENT') { @@ -387,7 +373,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows // (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams // (see https://github.com/microsoft/vscode/issues/6363) - await Promises.truncate(filePath, 0); + await promises.truncate(filePath, 0); // After a successful truncate() the flag can be set to 'r+' which will not truncate. flags = 'r+'; @@ -609,7 +595,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple async mkdir(resource: URI): Promise { try { - await Promises.mkdir(this.toFilePath(resource)); + await promises.mkdir(this.toFilePath(resource)); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -627,7 +613,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple await Promises.rm(filePath, RimRafMode.MOVE, rmMoveToPath); } else { try { - await Promises.unlink(filePath); + await promises.unlink(filePath); } catch (unlinkError) { // `fs.unlink` will throw when used on directories @@ -645,7 +631,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple } if (isDirectory) { - await Promises.rmdir(filePath); + await promises.rmdir(filePath); } else { throw unlinkError; } @@ -792,10 +778,10 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple locks.add(await this.createResourceLock(to)); if (mkdir) { - await Promises.mkdir(dirname(toFilePath), { recursive: true }); + await promises.mkdir(dirname(toFilePath), { recursive: true }); } - await Promises.copyFile(fromFilePath, toFilePath); + await promises.copyFile(fromFilePath, toFilePath); } catch (error) { if (error.code === 'ENOENT' && !mkdir) { return this.doCloneFile(from, to, true); diff --git a/src/vs/platform/files/node/watcher/baseWatcher.ts b/src/vs/platform/files/node/watcher/baseWatcher.ts index 3576f20d4f7..c5665396da9 100644 --- a/src/vs/platform/files/node/watcher/baseWatcher.ts +++ b/src/vs/platform/files/node/watcher/baseWatcher.ts @@ -9,7 +9,7 @@ import { ILogMessage, IRecursiveWatcherWithSubscribe, IUniversalWatchRequest, IW import { Emitter, Event } from 'vs/base/common/event'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; -import { DeferredPromise } from 'vs/base/common/async'; +import { DeferredPromise, ThrottledDelayer } from 'vs/base/common/async'; export abstract class BaseWatcher extends Disposable implements IWatcher { @@ -28,6 +28,8 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { private readonly suspendedWatchRequests = this._register(new DisposableMap()); private readonly suspendedWatchRequestsWithPolling = new Set(); + private readonly updateWatchersDelayer = this._register(new ThrottledDelayer(this.getUpdateWatchersDelay())); + protected readonly suspendedWatchRequestPollingInterval: number = 5007; // node.js default private joinWatch = new DeferredPromise(); @@ -88,17 +90,21 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { } } - return await this.updateWatchers(); + return await this.updateWatchers(false /* not delayed */); } finally { this.joinWatch.complete(); } } - private updateWatchers(): Promise { - return this.doWatch([ + private updateWatchers(delayed: boolean): Promise { + return this.updateWatchersDelayer.trigger(() => this.doWatch([ ...this.allNonCorrelatedWatchRequests, ...Array.from(this.allCorrelatedWatchRequests.values()).filter(request => !this.suspendedWatchRequests.has(request.correlationId)) - ]); + ]), delayed ? this.getUpdateWatchersDelay() : 0); + } + + protected getUpdateWatchersDelay(): number { + return 800; } isSuspended(request: IUniversalWatchRequest): 'polling' | boolean { @@ -130,14 +136,14 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { this.monitorSuspendedWatchRequest(request, disposables); - this.updateWatchers(); + this.updateWatchers(true /* delay this call as we might accumulate many failing watch requests on startup */); } private resumeWatchRequest(request: IWatchRequestWithCorrelation): void { this.suspendedWatchRequests.deleteAndDispose(request.correlationId); this.suspendedWatchRequestsWithPolling.delete(request.correlationId); - this.updateWatchers(); + this.updateWatchers(false); } private monitorSuspendedWatchRequest(request: IWatchRequestWithCorrelation, disposables: DisposableStore): void { diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index c71f1581cf6..eec6a2232c9 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { watch } from 'fs'; +import { watch, promises } from 'fs'; import { RunOnceWorker, ThrottledWorker } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { isEqualOrParent } from 'vs/base/common/extpath'; @@ -82,7 +82,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { return; } - const stat = await Promises.stat(realPath); + const stat = await promises.stat(realPath); if (this.cts.token.isCancellationRequested) { return; diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index afabd7aed12..46d213c12e8 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -226,7 +226,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS if (request.pollingInterval) { this.startPolling(request, request.pollingInterval); } else { - this.startWatching(request); + await this.startWatching(request); } } } @@ -322,7 +322,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS pollingWatcher.schedule(0); } - private startWatching(request: IRecursiveWatchRequest, restarts = 0): void { + private async startWatching(request: IRecursiveWatchRequest, restarts = 0): Promise { const cts = new CancellationTokenSource(); const instance = new DeferredPromise(); @@ -349,36 +349,38 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); - parcelWatcher.subscribe(realPath, (error, parcelEvents) => { - if (watcher.token.isCancellationRequested) { - return; // return early when disposed - } + try { + const parcelWatcherInstance = await parcelWatcher.subscribe(realPath, (error, parcelEvents) => { + if (watcher.token.isCancellationRequested) { + return; // return early when disposed + } - // In any case of an error, treat this like a unhandled exception - // that might require the watcher to restart. We do not really know - // the state of parcel at this point and as such will try to restart - // up to our maximum of restarts. - if (error) { - this.onUnexpectedError(error, request); - } + // In any case of an error, treat this like a unhandled exception + // that might require the watcher to restart. We do not really know + // the state of parcel at this point and as such will try to restart + // up to our maximum of restarts. + if (error) { + this.onUnexpectedError(error, request); + } + + // Handle & emit events + this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength); + }, { + backend: ParcelWatcher.PARCEL_WATCHER_BACKEND, + ignore: watcher.request.excludes + }); - // Handle & emit events - this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength); - }, { - backend: ParcelWatcher.PARCEL_WATCHER_BACKEND, - ignore: watcher.request.excludes - }).then(parcelWatcher => { this.trace(`Started watching: '${realPath}' with backend '${ParcelWatcher.PARCEL_WATCHER_BACKEND}'`); - instance.complete(parcelWatcher); - }).catch(error => { + instance.complete(parcelWatcherInstance); + } catch (error) { this.onUnexpectedError(error, request); instance.complete(undefined); watcher.notifyWatchFailed(); this._onDidWatchFail.fire(request); - }); + } } private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: ParcelWatcherInstance, realPathDiffers: boolean, realPathLength: number): void { @@ -662,7 +664,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS if (watcher.request.pollingInterval) { this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1); } else { - this.startWatching(watcher.request, watcher.restarts + 1); + await this.startWatching(watcher.request, watcher.restarts + 1); } } finally { restartPromise.complete(); diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index 114e98adefc..166cf549688 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DeferredPromise, timeout } from 'vs/base/common/async'; import { bufferToReadable, bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts b/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts index 9290520ecda..c5f542918ba 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IndexedDB } from 'vs/base/browser/indexedDB'; import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/platform/files/test/common/files.test.ts b/src/vs/platform/files/test/common/files.test.ts index 1de7f86398a..245d2222b9d 100644 --- a/src/vs/platform/files/test/common/files.test.ts +++ b/src/vs/platform/files/test/common/files.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isEqual, isEqualOrParent } from 'vs/base/common/extpath'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/files/test/common/watcher.test.ts b/src/vs/platform/files/test/common/watcher.test.ts index 23776f54d82..6d7bb7f3b83 100644 --- a/src/vs/platform/files/test/common/watcher.test.ts +++ b/src/vs/platform/files/test/common/watcher.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isLinux, isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts index fbf1ea0d870..c3c271abbdd 100644 --- a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts +++ b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { createReadStream, existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs'; +import assert from 'assert'; +import { createReadStream, existsSync, readdirSync, readFileSync, statSync, writeFileSync, promises } from 'fs'; import { tmpdir } from 'os'; import { timeout } from 'vs/base/common/async'; import { bufferToReadable, bufferToStream, streamToBuffer, streamToBufferReadableStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; @@ -429,7 +429,7 @@ flakySuite('Disk File Service', function () { test('resolve - folder symbolic link', async () => { const link = URI.file(join(testDir, 'deep-link')); - await Promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); + await promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); const resolved = await service.resolve(link); assert.strictEqual(resolved.children!.length, 4); @@ -439,7 +439,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => { const link = URI.file(join(testDir, 'lorem.txt-linked')); - await Promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); + await promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); const resolved = await service.resolve(link); assert.strictEqual(resolved.isDirectory, false); @@ -447,7 +447,7 @@ flakySuite('Disk File Service', function () { }); test('resolve - symbolic link pointing to nonexistent file does not break', async () => { - await Promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); + await promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); const resolved = await service.resolve(URI.file(testDir)); assert.strictEqual(resolved.isDirectory, true); @@ -530,7 +530,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (exists)', async () => { const target = URI.file(join(testDir, 'lorem.txt')); const link = URI.file(join(testDir, 'lorem.txt-linked')); - await Promises.symlink(target.fsPath, link.fsPath); + await promises.symlink(target.fsPath, link.fsPath); const source = await service.resolve(link); @@ -552,7 +552,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to nonexistent file)', async () => { const target = URI.file(join(testDir, 'foo')); const link = URI.file(join(testDir, 'bar')); - await Promises.symlink(target.fsPath, link.fsPath); + await promises.symlink(target.fsPath, link.fsPath); let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); @@ -1692,7 +1692,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => { const link = URI.file(join(testDir, 'small.js-link')); - await Promises.symlink(join(testDir, 'small.js'), link.fsPath); + await promises.symlink(join(testDir, 'small.js'), link.fsPath); let error: FileOperationError | undefined = undefined; try { @@ -1833,7 +1833,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('writeFile - atomic writing does not break symlinks', async () => { const link = URI.file(join(testDir, 'lorem.txt-linked')); - await Promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); + await promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); const content = 'Updates to the lorem file'; await service.writeFile(link, VSBuffer.fromString(content), { atomic: { postfix: '.vsctmp' } }); @@ -2006,7 +2006,7 @@ flakySuite('Disk File Service', function () { // Here since `close` is not called, all other writes are // waiting on the barrier to release, so doing a readFile // should give us a consistent view of the file contents - assert.strictEqual((await Promises.readFile(resource.fsPath)).toString(), content); + assert.strictEqual((await promises.readFile(resource.fsPath)).toString(), content); } finally { await provider.close(fd); } @@ -2030,10 +2030,10 @@ flakySuite('Disk File Service', function () { try { await provider.write(fd1, 0, VSBuffer.fromString(newContent).buffer, 0, VSBuffer.fromString(newContent).buffer.byteLength); - assert.strictEqual((await Promises.readFile(resource1.fsPath)).toString(), newContent); + assert.strictEqual((await promises.readFile(resource1.fsPath)).toString(), newContent); await provider.write(fd2, 0, VSBuffer.fromString(newContent).buffer, 0, VSBuffer.fromString(newContent).buffer.byteLength); - assert.strictEqual((await Promises.readFile(resource2.fsPath)).toString(), newContent); + assert.strictEqual((await promises.readFile(resource2.fsPath)).toString(), newContent); } finally { await Promise.allSettled([ await provider.close(fd1), @@ -2059,7 +2059,7 @@ flakySuite('Disk File Service', function () { assert.ok(error); // expected because `new-folder` does not exist - await Promises.mkdir(newFolder); + await promises.mkdir(newFolder); const content = readFileSync(URI.file(join(testDir, 'lorem.txt')).fsPath); const newContent = content.toString() + content.toString(); @@ -2069,7 +2069,7 @@ flakySuite('Disk File Service', function () { try { await provider.write(fd, 0, newContentBuffer, 0, newContentBuffer.byteLength); - assert.strictEqual((await Promises.readFile(newResource.fsPath)).toString(), newContent); + assert.strictEqual((await promises.readFile(newResource.fsPath)).toString(), newContent); } finally { await provider.close(fd); } @@ -2291,8 +2291,8 @@ flakySuite('Disk File Service', function () { const content = await service.writeFile(lockedFile, VSBuffer.fromString('Locked File')); assert.strictEqual(content.locked, false); - const stats = await Promises.stat(lockedFile.fsPath); - await Promises.chmod(lockedFile.fsPath, stats.mode & ~0o200); + const stats = await promises.stat(lockedFile.fsPath); + await promises.chmod(lockedFile.fsPath, stats.mode & ~0o200); let stat = await service.stat(lockedFile); assert.strictEqual(stat.locked, true); diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index 836b67ed9d2..c53f15426ab 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import * as fs from 'fs'; +import assert from 'assert'; import { tmpdir } from 'os'; import { basename, dirname, join } from 'vs/base/common/path'; import { Promises, RimRafMode } from 'vs/base/node/pfs'; -import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { FileChangeFilter, FileChangeType } from 'vs/platform/files/common/files'; import { INonRecursiveWatchRequest, IRecursiveWatcherWithSubscribe } from 'vs/platform/files/common/watcher'; import { watchFileContents } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; @@ -29,7 +30,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int // mocha but generally). as such they will run only on demand // whenever we update the watcher library. -((process.env['BUILD_SOURCEVERSION'] || process.env['CI']) ? suite.skip : flakySuite)('File Watcher (node.js)', () => { +suite.skip('File Watcher (node.js)', () => { class TestNodeJSWatcher extends NodeJSWatcher { @@ -40,6 +41,10 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int readonly onWatchFail = this._onDidWatchFail.event; + protected override getUpdateWatchersDelay(): number { + return 0; + } + protected override async doWatch(requests: INonRecursiveWatchRequest[]): Promise { await super.doWatch(requests); for (const watcher of this.watchers) { @@ -154,7 +159,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int // New folder const newFolderPath = join(testDir, 'New Folder'); changeFuture = awaitEvent(watcher, newFolderPath, FileChangeType.ADDED); - await Promises.mkdir(newFolderPath); + await fs.promises.mkdir(newFolderPath); await changeFuture; // Rename file @@ -216,7 +221,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int // Copy file const copiedFilepath = join(testDir, 'copiedFile.txt'); changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.ADDED); - await Promises.copyFile(movedFilepath, copiedFilepath); + await fs.promises.copyFile(movedFilepath, copiedFilepath); await changeFuture; // Copy folder @@ -238,12 +243,12 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int // Delete file changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.DELETED); - await Promises.unlink(copiedFilepath); + await fs.promises.unlink(copiedFilepath); await changeFuture; // Delete folder changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.DELETED); - await Promises.rmdir(copiedFolderpath); + await fs.promises.rmdir(copiedFolderpath); await changeFuture; watcher.dispose(); @@ -266,7 +271,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int // Delete file changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED); - await Promises.unlink(filePath); + await fs.promises.unlink(filePath); await changeFuture; // Recreate watcher @@ -286,7 +291,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int // Delete + Recreate file const newFilePath = join(testDir, 'lorem.txt'); const changeFuture: Promise = awaitEvent(watcher, newFilePath, FileChangeType.UPDATED); - await Promises.unlink(newFilePath); + await fs.promises.unlink(newFilePath); Promises.writeFile(newFilePath, 'Hello Atomic World'); await changeFuture; }); @@ -298,7 +303,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int // Delete + Recreate file const newFilePath = join(filePath); const changeFuture: Promise = awaitEvent(watcher, newFilePath, FileChangeType.UPDATED); - await Promises.unlink(newFilePath); + await fs.promises.unlink(newFilePath); Promises.writeFile(newFilePath, 'Hello Atomic World'); await changeFuture; }); @@ -359,9 +364,9 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int const deleteFuture3: Promise = awaitEvent(watcher, newFilePath3, FileChangeType.DELETED); await Promise.all([ - await Promises.unlink(newFilePath1), - await Promises.unlink(newFilePath2), - await Promises.unlink(newFilePath3) + await fs.promises.unlink(newFilePath1), + await fs.promises.unlink(newFilePath2), + await fs.promises.unlink(newFilePath3) ]); await Promise.all([deleteFuture1, deleteFuture2, deleteFuture3]); @@ -440,7 +445,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (folder watch)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); - await Promises.symlink(linkTarget, link); + await fs.promises.symlink(linkTarget, link); await watcher.watch([{ path: link, excludes: [], recursive: false }]); @@ -467,14 +472,14 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int // Delete file changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, correlationId, expectedCount); - await Promises.unlink(await Promises.realpath(filePath)); // support symlinks + await fs.promises.unlink(await Promises.realpath(filePath)); // support symlinks await changeFuture; } (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (file watch)', async function () { const link = join(testDir, 'lorem.txt-linked'); const linkTarget = join(testDir, 'lorem.txt'); - await Promises.symlink(linkTarget, link); + await fs.promises.symlink(linkTarget, link); await watcher.watch([{ path: link, excludes: [], recursive: false }]); @@ -579,7 +584,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int const onDidWatchFail = Event.toPromise(watcher.onWatchFail); const changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, 1); - Promises.unlink(filePath); + fs.promises.unlink(filePath); await onDidWatchFail; await changeFuture; assert.strictEqual(instance.failed, true); @@ -634,7 +639,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); let onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await fs.promises.mkdir(folderPath); await changeFuture; await onDidWatch; @@ -645,12 +650,12 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int if (!isMacintosh) { // macOS does not report DELETE events for folders onDidWatchFail = Event.toPromise(watcher.onWatchFail); - await Promises.rmdir(folderPath); + await fs.promises.rmdir(folderPath); await onDidWatchFail; changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await fs.promises.mkdir(folderPath); await changeFuture; await onDidWatch; @@ -673,7 +678,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); const onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await fs.promises.mkdir(folderPath); await changeFuture; await onDidWatch; @@ -773,7 +778,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int // Delete file changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, 1); - await Promises.unlink(filePath); + await fs.promises.unlink(filePath); await changeFuture; }); @@ -789,7 +794,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int // Delete file changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, 1); - await Promises.unlink(filePath); + await fs.promises.unlink(filePath); await changeFuture; }); }); diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index db85e28f341..4370d82bf90 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { realpathSync } from 'fs'; +import assert from 'assert'; +import { realpathSync, promises } from 'fs'; import { tmpdir } from 'os'; import { timeout } from 'vs/base/common/async'; import { dirname, join } from 'vs/base/common/path'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { Promises, RimRafMode } from 'vs/base/node/pfs'; -import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { FileChangeFilter, FileChangeType, IFileChange } from 'vs/platform/files/common/files'; import { ParcelWatcher } from 'vs/platform/files/node/watcher/parcel/parcelWatcher'; import { IRecursiveWatchRequest } from 'vs/platform/files/common/watcher'; @@ -42,6 +42,10 @@ export class TestParcelWatcher extends ParcelWatcher { return this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */).map(request => request.path); } + protected override getUpdateWatchersDelay(): number { + return 0; + } + protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise { await super.doWatch(requests); await this.whenReady(); @@ -61,7 +65,7 @@ export class TestParcelWatcher extends ParcelWatcher { // mocha but generally). as such they will run only on demand // whenever we update the watcher library. -((process.env['BUILD_SOURCEVERSION'] || process.env['CI']) ? suite.skip : flakySuite)('File Watcher (parcel)', () => { +suite.skip('File Watcher (parcel)', () => { let testDir: string; let watcher: TestParcelWatcher; @@ -216,7 +220,7 @@ export class TestParcelWatcher extends ParcelWatcher { assert.strictEqual(instance.include(newFolderPath), true); assert.strictEqual(instance.exclude(newFolderPath), false); changeFuture = awaitEvent(watcher, newFolderPath, FileChangeType.ADDED); - await Promises.mkdir(newFolderPath); + await promises.mkdir(newFolderPath); await changeFuture; assert.strictEqual(subscriptions1.get(newFolderPath), FileChangeType.ADDED); assert.strictEqual(subscriptions2.has(newFolderPath), false /* subscription was disposed before the event */); @@ -286,7 +290,7 @@ export class TestParcelWatcher extends ParcelWatcher { // Copy file const copiedFilepath = join(testDir, 'deep', 'copiedFile.txt'); changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.ADDED); - await Promises.copyFile(movedFilepath, copiedFilepath); + await promises.copyFile(movedFilepath, copiedFilepath); await changeFuture; // Copy folder @@ -308,30 +312,30 @@ export class TestParcelWatcher extends ParcelWatcher { // Read file does not emit event changeFuture = awaitEvent(watcher, anotherNewFilePath, FileChangeType.UPDATED, 'unexpected-event-from-read-file'); - await Promises.readFile(anotherNewFilePath); + await promises.readFile(anotherNewFilePath); await Promise.race([timeout(100), changeFuture]); // Stat file does not emit event changeFuture = awaitEvent(watcher, anotherNewFilePath, FileChangeType.UPDATED, 'unexpected-event-from-stat'); - await Promises.stat(anotherNewFilePath); + await promises.stat(anotherNewFilePath); await Promise.race([timeout(100), changeFuture]); // Stat folder does not emit event changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.UPDATED, 'unexpected-event-from-stat'); - await Promises.stat(copiedFolderpath); + await promises.stat(copiedFolderpath); await Promise.race([timeout(100), changeFuture]); // Delete file changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.DELETED); disposables.add(instance.subscribe(copiedFilepath, change => subscriptions1.set(change.resource.fsPath, change.type))); - await Promises.unlink(copiedFilepath); + await promises.unlink(copiedFilepath); await changeFuture; assert.strictEqual(subscriptions1.get(copiedFilepath), FileChangeType.DELETED); // Delete folder changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.DELETED); disposables.add(instance.subscribe(copiedFolderpath, change => subscriptions1.set(change.resource.fsPath, change.type))); - await Promises.rmdir(copiedFolderpath); + await promises.rmdir(copiedFolderpath); await changeFuture; assert.strictEqual(subscriptions1.get(copiedFolderpath), FileChangeType.DELETED); @@ -344,7 +348,7 @@ export class TestParcelWatcher extends ParcelWatcher { // Delete + Recreate file const newFilePath = join(testDir, 'deep', 'conway.js'); const changeFuture = awaitEvent(watcher, newFilePath, FileChangeType.UPDATED); - await Promises.unlink(newFilePath); + await promises.unlink(newFilePath); Promises.writeFile(newFilePath, 'Hello Atomic World'); await changeFuture; }); @@ -369,13 +373,13 @@ export class TestParcelWatcher extends ParcelWatcher { // Delete file changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, undefined, correlationId, expectedCount); - await Promises.unlink(filePath); + await promises.unlink(filePath); await changeFuture; } test('multiple events', async function () { await watcher.watch([{ path: testDir, excludes: [], recursive: true }]); - await Promises.mkdir(join(testDir, 'deep-multiple')); + await promises.mkdir(join(testDir, 'deep-multiple')); // multiple add @@ -445,12 +449,12 @@ export class TestParcelWatcher extends ParcelWatcher { const deleteFuture6 = awaitEvent(watcher, newFilePath6, FileChangeType.DELETED); await Promise.all([ - await Promises.unlink(newFilePath1), - await Promises.unlink(newFilePath2), - await Promises.unlink(newFilePath3), - await Promises.unlink(newFilePath4), - await Promises.unlink(newFilePath5), - await Promises.unlink(newFilePath6) + await promises.unlink(newFilePath1), + await promises.unlink(newFilePath2), + await promises.unlink(newFilePath3), + await promises.unlink(newFilePath4), + await promises.unlink(newFilePath5), + await promises.unlink(newFilePath6) ]); await Promise.all([deleteFuture1, deleteFuture2, deleteFuture3, deleteFuture4, deleteFuture5, deleteFuture6]); @@ -559,7 +563,7 @@ export class TestParcelWatcher extends ParcelWatcher { (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (root)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); - await Promises.symlink(linkTarget, link); + await promises.symlink(linkTarget, link); await watcher.watch([{ path: link, excludes: [], recursive: true }]); @@ -569,7 +573,7 @@ export class TestParcelWatcher extends ParcelWatcher { (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (via extra watch)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); - await Promises.symlink(linkTarget, link); + await promises.symlink(linkTarget, link); await watcher.watch([{ path: testDir, excludes: [], recursive: true }, { path: link, excludes: [], recursive: true }]); @@ -613,7 +617,7 @@ export class TestParcelWatcher extends ParcelWatcher { // Restore watched path await timeout(1500); // node.js watcher used for monitoring folder restore is async - await Promises.mkdir(watchedPath); + await promises.mkdir(watchedPath); await timeout(1500); // restart is delayed await watcher.whenReady(); @@ -772,7 +776,7 @@ export class TestParcelWatcher extends ParcelWatcher { let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); let onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await promises.mkdir(folderPath); await changeFuture; await onDidWatch; @@ -787,7 +791,7 @@ export class TestParcelWatcher extends ParcelWatcher { changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await promises.mkdir(folderPath); await changeFuture; await onDidWatch; @@ -821,7 +825,7 @@ export class TestParcelWatcher extends ParcelWatcher { const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); const onDidWatch = Event.toPromise(watcher.onDidWatch); - await Promises.mkdir(folderPath); + await promises.mkdir(folderPath); await changeFuture; await onDidWatch; @@ -860,7 +864,7 @@ export class TestParcelWatcher extends ParcelWatcher { // Delete file changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, undefined, 1); - await Promises.unlink(filePath); + await promises.unlink(filePath); await changeFuture; }); }); diff --git a/src/vs/platform/hover/browser/hover.ts b/src/vs/platform/hover/browser/hover.ts index b856f528c5b..51ead712fcb 100644 --- a/src/vs/platform/hover/browser/hover.ts +++ b/src/vs/platform/hover/browser/hover.ts @@ -7,7 +7,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { addStandardDisposableListener } from 'vs/base/browser/dom'; +import { addStandardDisposableListener, isHTMLElement } from 'vs/base/browser/dom'; import { KeyCode } from 'vs/base/common/keyCodes'; import type { IHoverDelegate2, IHoverOptions, IHoverWidget } from 'vs/base/browser/ui/hover/hover'; @@ -54,7 +54,7 @@ export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate // close hover on escape this.hoverDisposables.clear(); - const targets = options.target instanceof HTMLElement ? [options.target] : options.target.targetElements; + const targets = isHTMLElement(options.target) ? [options.target] : options.target.targetElements; for (const target of targets) { this.hoverDisposables.add(addStandardDisposableListener(target, 'keydown', (e) => { if (e.equals(KeyCode.Escape)) { @@ -63,7 +63,7 @@ export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate })); } - const id = options.content instanceof HTMLElement ? undefined : options.content.toString(); + const id = isHTMLElement(options.content) ? undefined : options.content.toString(); return this.hoverService.showHover({ ...options, diff --git a/src/vs/platform/hover/test/browser/nullHoverService.ts b/src/vs/platform/hover/test/browser/nullHoverService.ts index 6b7b728325b..44c73f8a0a6 100644 --- a/src/vs/platform/hover/test/browser/nullHoverService.ts +++ b/src/vs/platform/hover/test/browser/nullHoverService.ts @@ -10,7 +10,7 @@ export const NullHoverService: IHoverService = { _serviceBrand: undefined, hideHover: () => undefined, showHover: () => undefined, - setupUpdatableHover: () => Disposable.None as any, + setupManagedHover: () => Disposable.None as any, showAndFocusLastHover: () => undefined, - triggerUpdatableHover: () => undefined + showManagedHover: () => undefined }; diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index 4b313ed32eb..a61ef74333d 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -216,8 +216,15 @@ export class InstantiationService implements IInstantiationService { let cycleCount = 0; const stack = [{ id, desc, _trace }]; + const seen = new Set(); while (stack.length) { const item = stack.pop()!; + + if (seen.has(String(item.id))) { + continue; + } + seen.add(String(item.id)); + graph.lookupOrInsertNode(item); // a weak but working heuristic for cycle checks diff --git a/src/vs/platform/instantiation/test/common/graph.test.ts b/src/vs/platform/instantiation/test/common/graph.test.ts index c5abc22ccff..99e8e0e75a0 100644 --- a/src/vs/platform/instantiation/test/common/graph.test.ts +++ b/src/vs/platform/instantiation/test/common/graph.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Graph } from 'vs/platform/instantiation/common/graph'; diff --git a/src/vs/platform/instantiation/test/common/instantiationService.test.ts b/src/vs/platform/instantiation/test/common/instantiationService.test.ts index fd5bb77d2f7..70718ba712d 100644 --- a/src/vs/platform/instantiation/test/common/instantiationService.test.ts +++ b/src/vs/platform/instantiation/test/common/instantiationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index 81f3a8083ca..856c58c1873 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -127,18 +127,25 @@ export const IIssueMainService = createDecorator('issueServic export interface IIssueMainService { readonly _serviceBrand: undefined; - stopTracing(): Promise; - openReporter(data: IssueReporterData): Promise; - openProcessExplorer(data: ProcessExplorerData): Promise; - getSystemStatus(): Promise; // Used by the issue reporter - - $getSystemInfo(): Promise; - $getPerformanceInfo(): Promise; + openReporter(data: IssueReporterData): Promise; $reloadWithExtensionsDisabled(): Promise; $showConfirmCloseDialog(): Promise; $showClipboardDialog(): Promise; $sendReporterMenu(extensionId: string, extensionName: string): Promise; $closeReporter(): Promise; } + +export const IProcessMainService = createDecorator('processService'); + +export interface IProcessMainService { + readonly _serviceBrand: undefined; + getSystemStatus(): Promise; + stopTracing(): Promise; + openProcessExplorer(data: ProcessExplorerData): Promise; + + // Used by the process explorer + $getSystemInfo(): Promise; + $getPerformanceInfo(): Promise; +} diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index ee123897dd1..8f7ac7dfcc3 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -3,35 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, BrowserWindowConstructorOptions, contentTracing, Display, IpcMainEvent, screen } from 'electron'; +import { BrowserWindow, BrowserWindowConstructorOptions, Display, screen } from 'electron'; import { arch, release, type } from 'os'; import { raceTimeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { randomPath } from 'vs/base/common/extpath'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; -import { listProcesses } from 'vs/base/node/ps'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { localize } from 'vs/nls'; -import { IDiagnosticsService, isRemoteDiagnosticError, PerformanceInfo, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { IIssueMainService, IssueReporterData, IssueReporterWindowConfiguration, ProcessExplorerData, ProcessExplorerWindowConfiguration } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IssueReporterData, IssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; import { ILogService } from 'vs/platform/log/common/log'; import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; import product from 'vs/platform/product/common/product'; -import { IProductService } from 'vs/platform/product/common/productService'; import { IIPCObjectUrl, IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; -import { IStateService } from 'vs/platform/state/node/state'; -import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; import { zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; import { ICodeWindow, IWindowState } from 'vs/platform/window/electron-main/window'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -const processExplorerWindowState = 'issue.processExplorerWindowState'; - interface IBrowserWindowOptions { backgroundColor: string | undefined; title: string; @@ -50,94 +41,15 @@ export class IssueMainService implements IIssueMainService { private issueReporterWindow: BrowserWindow | null = null; private issueReporterParentWindow: BrowserWindow | null = null; - private processExplorerWindow: BrowserWindow | null = null; - private processExplorerParentWindow: BrowserWindow | null = null; - constructor( private userEnv: IProcessEnvironment, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILogService private readonly logService: ILogService, - @IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService, - @IDiagnosticsMainService private readonly diagnosticsMainService: IDiagnosticsMainService, @IDialogMainService private readonly dialogMainService: IDialogMainService, @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, @IProtocolMainService private readonly protocolMainService: IProtocolMainService, - @IProductService private readonly productService: IProductService, - @IStateService private readonly stateService: IStateService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - ) { - this.registerListeners(); - } - - //#region Register Listeners - - private registerListeners(): void { - validatedIpcMain.on('vscode:listProcesses', async event => { - const processes = []; - - try { - processes.push({ name: localize('local', "Local"), rootProcess: await listProcesses(process.pid) }); - - const remoteDiagnostics = await this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true }); - remoteDiagnostics.forEach(data => { - if (isRemoteDiagnosticError(data)) { - processes.push({ - name: data.hostName, - rootProcess: data - }); - } else { - if (data.processes) { - processes.push({ - name: data.hostName, - rootProcess: data.processes - }); - } - } - }); - } catch (e) { - this.logService.error(`Listing processes failed: ${e}`); - } - - this.safeSend(event, 'vscode:listProcessesResponse', processes); - }); - - validatedIpcMain.on('vscode:workbenchCommand', (_: unknown, commandInfo: { id: any; from: any; args: any }) => { - const { id, from, args } = commandInfo; - - let parentWindow: BrowserWindow | null; - switch (from) { - case 'processExplorer': - parentWindow = this.processExplorerParentWindow; - break; - default: - // The issue reporter does not use this anymore. - throw new Error(`Unexpected command source: ${from}`); - } - - parentWindow?.webContents.send('vscode:runAction', { id, from, args }); - }); - - validatedIpcMain.on('vscode:closeProcessExplorer', event => { - this.processExplorerWindow?.close(); - }); - - validatedIpcMain.on('vscode:pidToNameRequest', async event => { - const mainProcessInfo = await this.diagnosticsMainService.getMainDiagnostics(); - - const pidToNames: [number, string][] = []; - for (const window of mainProcessInfo.windows) { - pidToNames.push([window.pid, `window [${window.id}] (${window.title})`]); - } - - for (const { pid, name } of UtilityProcess.getAll()) { - pidToNames.push([pid, name]); - } - - this.safeSend(event, 'vscode:pidToNameResponse', pidToNames); - }); - } - - //#endregion + ) { } //#region Used by renderer @@ -169,11 +81,16 @@ export class IssueMainService implements IIssueMainService { arch: arch(), release: release(), }, - product + product, + nls: { + // VSCODE_GLOBALS: NLS + messages: globalThis._VSCODE_NLS_MESSAGES, + language: globalThis._VSCODE_NLS_LANGUAGE + } }); this.issueReporterWindow.loadURL( - FileAccess.asBrowserUri(`vs/code/electron-sandbox/issue/issueReporter${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true) + FileAccess.asBrowserUri(`vs/workbench/contrib/issue/electron-sandbox/issueReporter${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true) ); this.issueReporterWindow.on('close', () => { @@ -196,125 +113,9 @@ export class IssueMainService implements IIssueMainService { } } - async openProcessExplorer(data: ProcessExplorerData): Promise { - if (!this.processExplorerWindow) { - this.processExplorerParentWindow = BrowserWindow.getFocusedWindow(); - if (this.processExplorerParentWindow) { - const processExplorerDisposables = new DisposableStore(); - - const processExplorerWindowConfigUrl = processExplorerDisposables.add(this.protocolMainService.createIPCObjectUrl()); - - const savedPosition = this.stateService.getItem(processExplorerWindowState, undefined); - const position = isStrictWindowState(savedPosition) ? savedPosition : this.getWindowPosition(this.processExplorerParentWindow, 800, 500); - - this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, { - backgroundColor: data.styles.backgroundColor, - title: localize('processExplorer', "Process Explorer"), - zoomLevel: data.zoomLevel, - alwaysOnTop: true - }, 'process-explorer'); - - // Store into config object URL - processExplorerWindowConfigUrl.update({ - appRoot: this.environmentMainService.appRoot, - windowId: this.processExplorerWindow.id, - userEnv: this.userEnv, - data, - product - }); - - this.processExplorerWindow.loadURL( - FileAccess.asBrowserUri(`vs/code/electron-sandbox/processExplorer/processExplorer${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true) - ); - - this.processExplorerWindow.on('close', () => { - this.processExplorerWindow = null; - processExplorerDisposables.dispose(); - }); - - this.processExplorerParentWindow.on('close', () => { - if (this.processExplorerWindow) { - this.processExplorerWindow.close(); - this.processExplorerWindow = null; - - processExplorerDisposables.dispose(); - } - }); - - const storeState = () => { - if (!this.processExplorerWindow) { - return; - } - const size = this.processExplorerWindow.getSize(); - const position = this.processExplorerWindow.getPosition(); - if (!size || !position) { - return; - } - const state: IWindowState = { - width: size[0], - height: size[1], - x: position[0], - y: position[1] - }; - this.stateService.setItem(processExplorerWindowState, state); - }; - - this.processExplorerWindow.on('moved', storeState); - this.processExplorerWindow.on('resized', storeState); - } - } - - if (this.processExplorerWindow) { - this.focusWindow(this.processExplorerWindow); - } - } - - async stopTracing(): Promise { - if (!this.environmentMainService.args.trace) { - return; // requires tracing to be on - } - - const path = await contentTracing.stopRecording(`${randomPath(this.environmentMainService.userHome.fsPath, this.productService.applicationName)}.trace.txt`); - - // Inform user to report an issue - await this.dialogMainService.showMessageBox({ - type: 'info', - message: localize('trace.message', "Successfully created the trace file"), - detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), - buttons: [localize({ key: 'trace.ok', comment: ['&& denotes a mnemonic'] }, "&&OK")], - }, BrowserWindow.getFocusedWindow() ?? undefined); - - // Show item in explorer - this.nativeHostMainService.showItemInFolder(undefined, path); - } - - async getSystemStatus(): Promise { - const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); - - return this.diagnosticsService.getDiagnostics(info, remoteData); - } - //#endregion //#region used by issue reporter window - - async $getSystemInfo(): Promise { - const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); - const msg = await this.diagnosticsService.getSystemInfo(info, remoteData); - return msg; - } - - async $getPerformanceInfo(): Promise { - try { - const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true })]); - return await this.diagnosticsService.getPerformanceInfo(info, remoteData); - } catch (error) { - this.logService.warn('issueService#getPerformanceInfo ', error.message); - - throw error; - } - } - async $reloadWithExtensionsDisabled(): Promise { if (this.issueReporterParentWindow) { try { @@ -389,10 +190,6 @@ export class IssueMainService implements IIssueMainService { this.issueReporterWindow?.close(); } - async closeProcessExplorer(): Promise { - this.processExplorerWindow?.close(); - } - //#endregion private focusWindow(window: BrowserWindow): void { @@ -403,12 +200,6 @@ export class IssueMainService implements IIssueMainService { window.focus(); } - private safeSend(event: IpcMainEvent, channel: string, ...args: unknown[]): void { - if (!event.sender.isDestroyed()) { - event.sender.send(channel, ...args); - } - } - private createBrowserWindow(position: IWindowState, ipcObjectUrl: IIPCObjectUrl, options: IBrowserWindowOptions, windowKind: string): BrowserWindow { const window = new BrowserWindow({ fullscreen: false, @@ -509,15 +300,3 @@ export class IssueMainService implements IIssueMainService { return state; } } - -function isStrictWindowState(obj: unknown): obj is IStrictWindowState { - if (typeof obj !== 'object' || obj === null) { - return false; - } - return ( - 'x' in obj && - 'y' in obj && - 'width' in obj && - 'height' in obj - ); -} diff --git a/src/vs/platform/issue/electron-main/processMainService.ts b/src/vs/platform/issue/electron-main/processMainService.ts new file mode 100644 index 00000000000..76da45d897c --- /dev/null +++ b/src/vs/platform/issue/electron-main/processMainService.ts @@ -0,0 +1,380 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BrowserWindow, BrowserWindowConstructorOptions, contentTracing, Display, IpcMainEvent, screen } from 'electron'; +import { randomPath } from 'vs/base/common/extpath'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; +import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; +import { listProcesses } from 'vs/base/node/ps'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; +import { localize } from 'vs/nls'; +import { IDiagnosticsService, isRemoteDiagnosticError, PerformanceInfo, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { IProcessMainService, ProcessExplorerData, ProcessExplorerWindowConfiguration } from 'vs/platform/issue/common/issue'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; +import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IIPCObjectUrl, IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; +import { IStateService } from 'vs/platform/state/node/state'; +import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; +import { zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; +import { IWindowState } from 'vs/platform/window/electron-main/window'; + +const processExplorerWindowState = 'issue.processExplorerWindowState'; + +interface IBrowserWindowOptions { + backgroundColor: string | undefined; + title: string; + zoomLevel: number; + alwaysOnTop: boolean; +} + +type IStrictWindowState = Required>; + +export class ProcessMainService implements IProcessMainService { + + declare readonly _serviceBrand: undefined; + + private static readonly DEFAULT_BACKGROUND_COLOR = '#1E1E1E'; + + private processExplorerWindow: BrowserWindow | null = null; + private processExplorerParentWindow: BrowserWindow | null = null; + + constructor( + private userEnv: IProcessEnvironment, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, + @ILogService private readonly logService: ILogService, + @IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService, + @IDiagnosticsMainService private readonly diagnosticsMainService: IDiagnosticsMainService, + @IDialogMainService private readonly dialogMainService: IDialogMainService, + @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, + @IProtocolMainService private readonly protocolMainService: IProtocolMainService, + @IProductService private readonly productService: IProductService, + @IStateService private readonly stateService: IStateService, + ) { + this.registerListeners(); + } + + //#region Register Listeners + + private registerListeners(): void { + validatedIpcMain.on('vscode:listProcesses', async event => { + const processes = []; + + try { + processes.push({ name: localize('local', "Local"), rootProcess: await listProcesses(process.pid) }); + + const remoteDiagnostics = await this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true }); + remoteDiagnostics.forEach(data => { + if (isRemoteDiagnosticError(data)) { + processes.push({ + name: data.hostName, + rootProcess: data + }); + } else { + if (data.processes) { + processes.push({ + name: data.hostName, + rootProcess: data.processes + }); + } + } + }); + } catch (e) { + this.logService.error(`Listing processes failed: ${e}`); + } + + this.safeSend(event, 'vscode:listProcessesResponse', processes); + }); + + validatedIpcMain.on('vscode:workbenchCommand', (_: unknown, commandInfo: { id: any; from: any; args: any }) => { + const { id, from, args } = commandInfo; + + let parentWindow: BrowserWindow | null; + switch (from) { + case 'processExplorer': + parentWindow = this.processExplorerParentWindow; + break; + default: + // The issue reporter does not use this anymore. + throw new Error(`Unexpected command source: ${from}`); + } + + parentWindow?.webContents.send('vscode:runAction', { id, from, args }); + }); + + validatedIpcMain.on('vscode:closeProcessExplorer', event => { + this.processExplorerWindow?.close(); + }); + + validatedIpcMain.on('vscode:pidToNameRequest', async event => { + const mainProcessInfo = await this.diagnosticsMainService.getMainDiagnostics(); + + const pidToNames: [number, string][] = []; + for (const window of mainProcessInfo.windows) { + pidToNames.push([window.pid, `window [${window.id}] (${window.title})`]); + } + + for (const { pid, name } of UtilityProcess.getAll()) { + pidToNames.push([pid, name]); + } + + this.safeSend(event, 'vscode:pidToNameResponse', pidToNames); + }); + } + + async openProcessExplorer(data: ProcessExplorerData): Promise { + if (!this.processExplorerWindow) { + this.processExplorerParentWindow = BrowserWindow.getFocusedWindow(); + if (this.processExplorerParentWindow) { + const processExplorerDisposables = new DisposableStore(); + + const processExplorerWindowConfigUrl = processExplorerDisposables.add(this.protocolMainService.createIPCObjectUrl()); + + const savedPosition = this.stateService.getItem(processExplorerWindowState, undefined); + const position = isStrictWindowState(savedPosition) ? savedPosition : this.getWindowPosition(this.processExplorerParentWindow, 800, 500); + + this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, { + backgroundColor: data.styles.backgroundColor, + title: localize('processExplorer', "Process Explorer"), + zoomLevel: data.zoomLevel, + alwaysOnTop: true + }, 'process-explorer'); + + // Store into config object URL + processExplorerWindowConfigUrl.update({ + appRoot: this.environmentMainService.appRoot, + windowId: this.processExplorerWindow.id, + userEnv: this.userEnv, + data, + product, + nls: { + // VSCODE_GLOBALS: NLS + messages: globalThis._VSCODE_NLS_MESSAGES, + language: globalThis._VSCODE_NLS_LANGUAGE + } + }); + + this.processExplorerWindow.loadURL( + FileAccess.asBrowserUri(`vs/code/electron-sandbox/processExplorer/processExplorer${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true) + ); + + this.processExplorerWindow.on('close', () => { + this.processExplorerWindow = null; + processExplorerDisposables.dispose(); + }); + + this.processExplorerParentWindow.on('close', () => { + if (this.processExplorerWindow) { + this.processExplorerWindow.close(); + this.processExplorerWindow = null; + + processExplorerDisposables.dispose(); + } + }); + + const storeState = () => { + if (!this.processExplorerWindow) { + return; + } + const size = this.processExplorerWindow.getSize(); + const position = this.processExplorerWindow.getPosition(); + if (!size || !position) { + return; + } + const state: IWindowState = { + width: size[0], + height: size[1], + x: position[0], + y: position[1] + }; + this.stateService.setItem(processExplorerWindowState, state); + }; + + this.processExplorerWindow.on('moved', storeState); + this.processExplorerWindow.on('resized', storeState); + } + } + + if (this.processExplorerWindow) { + this.focusWindow(this.processExplorerWindow); + } + } + + private focusWindow(window: BrowserWindow): void { + if (window.isMinimized()) { + window.restore(); + } + + window.focus(); + } + + private getWindowPosition(parentWindow: BrowserWindow, defaultWidth: number, defaultHeight: number): IStrictWindowState { + + // We want the new window to open on the same display that the parent is in + let displayToUse: Display | undefined; + const displays = screen.getAllDisplays(); + + // Single Display + if (displays.length === 1) { + displayToUse = displays[0]; + } + + // Multi Display + else { + + // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is + if (isMacintosh) { + const cursorPoint = screen.getCursorScreenPoint(); + displayToUse = screen.getDisplayNearestPoint(cursorPoint); + } + + // if we have a last active window, use that display for the new window + if (!displayToUse && parentWindow) { + displayToUse = screen.getDisplayMatching(parentWindow.getBounds()); + } + + // fallback to primary display or first display + if (!displayToUse) { + displayToUse = screen.getPrimaryDisplay() || displays[0]; + } + } + + const displayBounds = displayToUse.bounds; + + const state: IStrictWindowState = { + width: defaultWidth, + height: defaultHeight, + x: displayBounds.x + (displayBounds.width / 2) - (defaultWidth / 2), + y: displayBounds.y + (displayBounds.height / 2) - (defaultHeight / 2) + }; + + if (displayBounds.width > 0 && displayBounds.height > 0 /* Linux X11 sessions sometimes report wrong display bounds */) { + if (state.x < displayBounds.x) { + state.x = displayBounds.x; // prevent window from falling out of the screen to the left + } + + if (state.y < displayBounds.y) { + state.y = displayBounds.y; // prevent window from falling out of the screen to the top + } + + if (state.x > (displayBounds.x + displayBounds.width)) { + state.x = displayBounds.x; // prevent window from falling out of the screen to the right + } + + if (state.y > (displayBounds.y + displayBounds.height)) { + state.y = displayBounds.y; // prevent window from falling out of the screen to the bottom + } + + if (state.width > displayBounds.width) { + state.width = displayBounds.width; // prevent window from exceeding display bounds width + } + + if (state.height > displayBounds.height) { + state.height = displayBounds.height; // prevent window from exceeding display bounds height + } + } + + return state; + } + + async stopTracing(): Promise { + if (!this.environmentMainService.args.trace) { + return; // requires tracing to be on + } + + const path = await contentTracing.stopRecording(`${randomPath(this.environmentMainService.userHome.fsPath, this.productService.applicationName)}.trace.txt`); + + // Inform user to report an issue + await this.dialogMainService.showMessageBox({ + type: 'info', + message: localize('trace.message', "Successfully created the trace file"), + detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), + buttons: [localize({ key: 'trace.ok', comment: ['&& denotes a mnemonic'] }, "&&OK")], + }, BrowserWindow.getFocusedWindow() ?? undefined); + + // Show item in explorer + this.nativeHostMainService.showItemInFolder(undefined, path); + } + + async getSystemStatus(): Promise { + const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); + return this.diagnosticsService.getDiagnostics(info, remoteData); + } + + async $getSystemInfo(): Promise { + const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); + const msg = await this.diagnosticsService.getSystemInfo(info, remoteData); + return msg; + } + + async $getPerformanceInfo(): Promise { + try { + const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true })]); + return await this.diagnosticsService.getPerformanceInfo(info, remoteData); + } catch (error) { + this.logService.warn('issueService#getPerformanceInfo ', error.message); + + throw error; + } + } + + private createBrowserWindow(position: IWindowState, ipcObjectUrl: IIPCObjectUrl, options: IBrowserWindowOptions, windowKind: string): BrowserWindow { + const window = new BrowserWindow({ + fullscreen: false, + skipTaskbar: false, + resizable: true, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + title: options.title, + backgroundColor: options.backgroundColor || ProcessMainService.DEFAULT_BACKGROUND_COLOR, + webPreferences: { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload.js').fsPath, + additionalArguments: [`--vscode-window-config=${ipcObjectUrl.resource.toString()}`], + v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none', + enableWebSQL: false, + spellcheck: false, + zoomFactor: zoomLevelToZoomFactor(options.zoomLevel), + sandbox: true + }, + alwaysOnTop: options.alwaysOnTop, + experimentalDarkMode: true + } as BrowserWindowConstructorOptions & { experimentalDarkMode: boolean }); + + window.setMenuBarVisibility(false); + + return window; + } + + private safeSend(event: IpcMainEvent, channel: string, ...args: unknown[]): void { + if (!event.sender.isDestroyed()) { + event.sender.send(channel, ...args); + } + } + + async closeProcessExplorer(): Promise { + this.processExplorerWindow?.close(); + } +} + +function isStrictWindowState(obj: unknown): obj is IStrictWindowState { + if (typeof obj !== 'object' || obj === null) { + return false; + } + return ( + 'x' in obj && + 'y' in obj && + 'width' in obj && + 'height' in obj + ); +} diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 2f79c811d5e..88aa86a05d3 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { createSimpleKeybinding, ResolvedKeybinding, KeyCodeChord, Keybinding } from 'vs/base/common/keybindings'; import { Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts index dfc5df1e096..b88a747e454 100644 --- a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { OperatingSystem } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts index 54bd2a6761d..3fccf387540 100644 --- a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { decodeKeybinding, createSimpleKeybinding, KeyCodeChord } from 'vs/base/common/keybindings'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { OS } from 'vs/base/common/platform'; diff --git a/src/vs/platform/languagePacks/node/languagePacks.ts b/src/vs/platform/languagePacks/node/languagePacks.ts index 3a7df771bc2..698b9b56de7 100644 --- a/src/vs/platform/languagePacks/node/languagePacks.ts +++ b/src/vs/platform/languagePacks/node/languagePacks.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { createHash } from 'crypto'; import { equals } from 'vs/base/common/arrays'; import { Queue } from 'vs/base/common/async'; @@ -180,7 +181,7 @@ class LanguagePacksCache extends Disposable { private withLanguagePacks(fn: (languagePacks: { [language: string]: ILanguagePack }) => T | null = () => null): Promise { return this.languagePacksFileLimiter.queue(() => { let result: T | null = null; - return Promises.readFile(this.languagePacksFilePath, 'utf8') + return fs.promises.readFile(this.languagePacksFilePath, 'utf8') .then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err)) .then<{ [language: string]: ILanguagePack }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } }) .then(languagePacks => { result = fn(languagePacks); return languagePacks; }) diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts index 7dfc46d0a3b..4013b2b548b 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, Event as ElectronEvent } from 'electron'; +import electron from 'electron'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { Barrier, Promises, timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -294,7 +294,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe this.fireOnWillShutdown(ShutdownReason.QUIT); } }; - app.addListener('before-quit', beforeQuitListener); + electron.app.addListener('before-quit', beforeQuitListener); // window-all-closed: an event that only fires when the last window // was closed. We override this event to be in charge if app.quit() @@ -305,14 +305,14 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe // Windows/Linux: we quit when all windows have closed // Mac: we only quit when quit was requested if (this._quitRequested || !isMacintosh) { - app.quit(); + electron.app.quit(); } }; - app.addListener('window-all-closed', windowAllClosedListener); + electron.app.addListener('window-all-closed', windowAllClosedListener); // will-quit: an event that is fired after all windows have been // closed, but before actually quitting. - app.once('will-quit', e => { + electron.app.once('will-quit', e => { this.trace('Lifecycle#app.on(will-quit) - begin'); // Prevent the quit until the shutdown promise was resolved @@ -332,12 +332,12 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe // will-quit listener is only installed "once". Also // remove any listener we have that is no longer needed - app.removeListener('before-quit', beforeQuitListener); - app.removeListener('window-all-closed', windowAllClosedListener); + electron.app.removeListener('before-quit', beforeQuitListener); + electron.app.removeListener('window-all-closed', windowAllClosedListener); this.trace('Lifecycle#app.on(will-quit) - calling app.quit()'); - app.quit(); + electron.app.quit(); }); }); } @@ -428,7 +428,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe // Window Before Closing: Main -> Renderer const win = assertIsDefined(window.win); - windowListeners.add(Event.fromNodeEventEmitter(win, 'close')(e => { + windowListeners.add(Event.fromNodeEventEmitter(win, 'close')(e => { // The window already acknowledged to be closed const windowId = window.id; @@ -458,7 +458,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe window.close(); }); })); - windowListeners.add(Event.fromNodeEventEmitter(win, 'closed')(() => { + windowListeners.add(Event.fromNodeEventEmitter(win, 'closed')(() => { this.trace(`Lifecycle#window.on('closed') - window ID ${window.id}`); // update window count @@ -480,7 +480,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe const win = assertIsDefined(auxWindow.win); const windowListeners = new DisposableStore(); - windowListeners.add(Event.fromNodeEventEmitter(win, 'close')(e => { + windowListeners.add(Event.fromNodeEventEmitter(win, 'close')(e => { this.trace(`Lifecycle#auxWindow.on('close') - window ID ${auxWindow.id}`); if (this._quitRequested) { @@ -499,7 +499,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe e.preventDefault(); } })); - windowListeners.add(Event.fromNodeEventEmitter(win, 'closed')(() => { + windowListeners.add(Event.fromNodeEventEmitter(win, 'closed')(() => { this.trace(`Lifecycle#auxWindow.on('closed') - window ID ${auxWindow.id}`); windowListeners.dispose(); @@ -652,7 +652,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe // Calling app.quit() will trigger the close handlers of each opened window // and only if no window vetoed the shutdown, we will get the will-quit event this.trace('Lifecycle#quit() - calling app.quit()'); - app.quit(); + electron.app.quit(); }); return this.pendingQuitPromise; @@ -690,16 +690,16 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe const quitListener = () => { if (!this.relaunchHandler?.handleRelaunch(options)) { this.trace('Lifecycle#relaunch() - calling app.relaunch()'); - app.relaunch({ args }); + electron.app.relaunch({ args }); } }; - app.once('quit', quitListener); + electron.app.once('quit', quitListener); // `app.relaunch()` does not quit automatically, so we quit first, // check for vetoes and then relaunch from the `app.on('quit')` event const veto = await this.quit(true /* will restart */); if (veto) { - app.removeListener('quit', quitListener); + electron.app.removeListener('quit', quitListener); } } @@ -727,7 +727,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe // to a participant within the window. this is not wanted when we // are asked to kill the application. (async () => { - for (const window of BrowserWindow.getAllWindows()) { + for (const window of electron.BrowserWindow.getAllWindows()) { if (window && !window.isDestroyed()) { let whenWindowClosed: Promise; if (window.webContents && !window.webContents.isDestroyed()) { @@ -744,6 +744,6 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe ]); // Now exit either after 1s or all windows destroyed - app.exit(code); + electron.app.exit(code); } } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 6857531cb66..976b9a85dc3 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -1490,7 +1490,7 @@ configurationRegistry.registerConfiguration({ type: 'number', minimum: 1, default: 7, - markdownDescription: localize('sticky scroll maximum items', "Controls the number of sticky elements displayed in the tree when `#workbench.tree.enableStickyScroll#` is enabled."), + markdownDescription: localize('sticky scroll maximum items', "Controls the number of sticky elements displayed in the tree when {0} is enabled.", '`#workbench.tree.enableStickyScroll#`'), }, [typeNavigationModeSettingKey]: { type: 'string', diff --git a/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index 91120aab5b6..692f6ba5cb7 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/src/vs/platform/markers/common/markerService.ts @@ -18,6 +18,7 @@ export const unsupportedSchemas = new Set([ Schemas.walkThrough, Schemas.walkThroughSnippet, Schemas.vscodeChatCodeBlock, + Schemas.vscodeCopilotBackingChatCodeBlock, ]); class DoubleResourceMap { diff --git a/src/vs/platform/markers/test/common/markerService.test.ts b/src/vs/platform/markers/test/common/markerService.test.ts index d8ebccf746f..1b9916a8adf 100644 --- a/src/vs/platform/markers/test/common/markerService.test.ts +++ b/src/vs/platform/markers/test/common/markerService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 7e96549a723..d7449e94541 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -25,6 +25,7 @@ import { IUpdateService, StateType } from 'vs/platform/update/common/update'; import { INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable, hasNativeTitlebar } from 'vs/platform/window/common/window'; import { IWindowsCountChangedEvent, IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; +import { Disposable } from 'vs/base/common/lifecycle'; const telemetryFrom = 'menu'; @@ -42,7 +43,7 @@ interface IMenuItemWithKeybinding { userSettingsLabel?: string; } -export class Menubar { +export class Menubar extends Disposable { private static readonly lastKnownMenubarStorageKey = 'lastKnownMenubarData'; @@ -78,6 +79,8 @@ export class Menubar { @IProductService private readonly productService: IProductService, @IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService ) { + super(); + this.menuUpdater = new RunOnceScheduler(() => this.doUpdateMenu(), 0); this.menuGC = new RunOnceScheduler(() => { this.oldMenus = []; }, 10000); @@ -169,12 +172,12 @@ export class Menubar { private registerListeners(): void { // Keep flag when app quits - this.lifecycleMainService.onWillShutdown(() => this.willShutdown = true); + this._register(this.lifecycleMainService.onWillShutdown(() => this.willShutdown = true)); // Listen to some events from window service to update menu - this.windowsMainService.onDidChangeWindowsCount(e => this.onDidChangeWindowsCount(e)); - this.nativeHostMainService.onDidBlurMainWindow(() => this.onDidChangeWindowFocus()); - this.nativeHostMainService.onDidFocusMainWindow(() => this.onDidChangeWindowFocus()); + this._register(this.windowsMainService.onDidChangeWindowsCount(e => this.onDidChangeWindowsCount(e))); + this._register(this.nativeHostMainService.onDidBlurMainWindow(() => this.onDidChangeWindowFocus())); + this._register(this.nativeHostMainService.onDidFocusMainWindow(() => this.onDidChangeWindowFocus())); } private get currentEnableMenuBarMnemonics(): boolean { diff --git a/src/vs/platform/menubar/electron-main/menubarMainService.ts b/src/vs/platform/menubar/electron-main/menubarMainService.ts index c680afa296a..731070e4f50 100644 --- a/src/vs/platform/menubar/electron-main/menubarMainService.ts +++ b/src/vs/platform/menubar/electron-main/menubarMainService.ts @@ -8,6 +8,7 @@ import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle import { ILogService } from 'vs/platform/log/common/log'; import { ICommonMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar'; import { Menubar } from 'vs/platform/menubar/electron-main/menubar'; +import { Disposable } from 'vs/base/common/lifecycle'; export const IMenubarMainService = createDecorator('menubarMainService'); @@ -15,24 +16,24 @@ export interface IMenubarMainService extends ICommonMenubarService { readonly _serviceBrand: undefined; } -export class MenubarMainService implements IMenubarMainService { +export class MenubarMainService extends Disposable implements IMenubarMainService { declare readonly _serviceBrand: undefined; - private menubar: Promise; + private readonly menubar = this.installMenuBarAfterWindowOpen(); constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @ILogService private readonly logService: ILogService ) { - this.menubar = this.installMenuBarAfterWindowOpen(); + super(); } private async installMenuBarAfterWindowOpen(): Promise { await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen); - return this.instantiationService.createInstance(Menubar); + return this._register(this.instantiationService.createInstance(Menubar)); } async updateMenubar(windowId: number, menus: IMenubarData): Promise { diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 85e831fa04a..8a45868ed63 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -11,6 +11,7 @@ import { ISerializableCommandAction } from 'vs/platform/action/common/action'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IV8Profile } from 'vs/platform/profiling/common/profiling'; +import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable } from 'vs/platform/window/common/window'; @@ -126,7 +127,7 @@ export interface ICommonNativeHostService { showItemInFolder(path: string): Promise; setRepresentedFilename(path: string, options?: INativeHostOptions): Promise; setDocumentEdited(edited: boolean, options?: INativeHostOptions): Promise; - openExternal(url: string): Promise; + openExternal(url: string, defaultApplication?: string): Promise; moveItemToTrash(fullPath: string): Promise; isAdmin(): Promise; @@ -142,6 +143,7 @@ export interface ICommonNativeHostService { hasWSLFeatureInstalled(): Promise; // Process + getProcessId(): Promise; killProcess(pid: number, code: string): Promise; // Clipboard @@ -183,6 +185,7 @@ export interface ICommonNativeHostService { // Connectivity resolveProxy(url: string): Promise; + lookupAuthorization(authInfo: AuthInfo): Promise; loadCertificates(): Promise; findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride?: number): Promise; diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/platform/native/electron-main/auth.ts similarity index 54% rename from src/vs/code/electron-main/auth.ts rename to src/vs/platform/native/electron-main/auth.ts index bf2a99f601c..15918242bba 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/platform/native/electron-main/auth.ts @@ -3,14 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, AuthenticationResponseDetails, AuthInfo, Event as ElectronEvent, WebContents } from 'electron'; +import { app, AuthenticationResponseDetails, AuthInfo as ElectronAuthInfo, Event as ElectronEvent, WebContents } from 'electron'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IApplicationStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; @@ -20,55 +25,37 @@ interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDe } type LoginEvent = { - event: ElectronEvent; + event?: ElectronEvent; authInfo: AuthInfo; - req: ElectronAuthenticationResponseDetails; - - callback: (username?: string, password?: string) => void; + callback?: (username?: string, password?: string) => void; }; -type Credentials = { - username: string; - password: string; -}; +export const IProxyAuthService = createDecorator('proxyAuthService'); -enum ProxyAuthState { - - /** - * Initial state: we will try to use stored credentials - * first to reply to the auth challenge. - */ - Initial = 1, - - /** - * We used stored credentials and are still challenged, - * so we will show a login dialog next. - */ - StoredCredentialsUsed, - - /** - * Finally, if we showed a login dialog already, we will - * not show any more login dialogs until restart to reduce - * the UI noise. - */ - LoginDialogShown +export interface IProxyAuthService { + lookupAuthorization(authInfo: AuthInfo): Promise; } -export class ProxyAuthHandler extends Disposable { +export class ProxyAuthService extends Disposable implements IProxyAuthService { + + declare readonly _serviceBrand: undefined; private readonly PROXY_CREDENTIALS_SERVICE_KEY = 'proxy-credentials://'; - private pendingProxyResolve: Promise | undefined = undefined; + private pendingProxyResolves = new Map>(); + private currentDialog: Promise | undefined = undefined; - private state = ProxyAuthState.Initial; + private cancelledAuthInfoHashes = new Set(); - private sessionCredentials: Credentials | undefined = undefined; + private sessionCredentials = new Map(); constructor( @ILogService private readonly logService: ILogService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService, - @IApplicationStorageMainService private readonly applicationStorageMainService: IApplicationStorageMainService + @IApplicationStorageMainService private readonly applicationStorageMainService: IApplicationStorageMainService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, ) { super(); @@ -76,39 +63,45 @@ export class ProxyAuthHandler extends Disposable { } private registerListeners(): void { - const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback })); + const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, _webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: ElectronAuthInfo, callback) => ({ event, authInfo: { ...authInfo, attempt: req.firstAuthAttempt ? 1 : 2 }, callback } satisfies LoginEvent)); this._register(onLogin(this.onLogin, this)); } - private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise { + async lookupAuthorization(authInfo: AuthInfo): Promise { + return this.onLogin({ authInfo }); + } + + private async onLogin({ event, authInfo, callback }: LoginEvent): Promise { if (!authInfo.isProxy) { return; // only for proxy } - if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) { - this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown'); - - return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem) - } - // Signal we handle this event on our own, otherwise // Electron will ignore our provided credentials. - event.preventDefault(); + event?.preventDefault(); + + // Compute a hash over the authentication info to be used + // with the credentials store to return the right credentials + // given the properties of the auth request + // (see https://github.com/microsoft/vscode/issues/109497) + const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); let credentials: Credentials | undefined = undefined; - if (!this.pendingProxyResolve) { + let pendingProxyResolve = this.pendingProxyResolves.get(authInfoHash); + if (!pendingProxyResolve) { this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new'); - this.pendingProxyResolve = this.resolveProxyCredentials(authInfo); + pendingProxyResolve = this.resolveProxyCredentials(authInfo, authInfoHash); + this.pendingProxyResolves.set(authInfoHash, pendingProxyResolve); try { - credentials = await this.pendingProxyResolve; + credentials = await pendingProxyResolve; } finally { - this.pendingProxyResolve = undefined; + this.pendingProxyResolves.delete(authInfoHash); } } else { this.logService.trace('auth#onLogin (proxy) - pending proxy handling found'); - credentials = await this.pendingProxyResolve; + credentials = await pendingProxyResolve; } // According to Electron docs, it is fine to call back without @@ -118,14 +111,15 @@ export class ProxyAuthHandler extends Disposable { // > If `callback` is called without a username or password, the authentication // > request will be cancelled and the authentication error will be returned to the // > page. - callback(credentials?.username, credentials?.password); + callback?.(credentials?.username, credentials?.password); + return credentials; } - private async resolveProxyCredentials(authInfo: AuthInfo): Promise { + private async resolveProxyCredentials(authInfo: AuthInfo, authInfoHash: string): Promise { this.logService.trace('auth#resolveProxyCredentials (proxy) - enter'); try { - const credentials = await this.doResolveProxyCredentials(authInfo); + const credentials = await this.doResolveProxyCredentials(authInfo, authInfoHash); if (credentials) { this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials'); @@ -140,14 +134,68 @@ export class ProxyAuthHandler extends Disposable { return undefined; } - private async doResolveProxyCredentials(authInfo: AuthInfo): Promise { + private async doResolveProxyCredentials(authInfo: AuthInfo, authInfoHash: string): Promise { this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo); - // Compute a hash over the authentication info to be used - // with the credentials store to return the right credentials - // given the properties of the auth request - // (see https://github.com/microsoft/vscode/issues/109497) - const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); + // For testing. + if (this.environmentMainService.extensionTestsLocationURI) { + const credentials = this.configurationService.getValue('integration-test.http.proxyAuth'); + if (credentials) { + const j = credentials.indexOf(':'); + if (j !== -1) { + return { + username: credentials.substring(0, j), + password: credentials.substring(j + 1) + }; + } else { + return { + username: credentials, + password: '' + }; + } + } + return undefined; + } + + // Reply with manually supplied credentials. Fail if they are wrong. + const newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() + || (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() + || undefined; + + if (newHttpProxy?.indexOf('@') !== -1) { + const uri = URI.parse(newHttpProxy!); + const i = uri.authority.indexOf('@'); + if (i !== -1) { + if (authInfo.attempt > 1) { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - ignoring previously used config/envvar credentials'); + return undefined; // We tried already, let the user handle it. + } + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found config/envvar credentials to use'); + const credentials = uri.authority.substring(0, i); + const j = credentials.indexOf(':'); + if (j !== -1) { + return { + username: credentials.substring(0, j), + password: credentials.substring(j + 1) + }; + } else { + return { + username: credentials, + password: '' + }; + } + } + } + + // Reply with session credentials unless we used them already. + // In that case we need to show a login dialog again because + // they seem invalid. + if (authInfo.attempt === 1 && this.sessionCredentials.has(authInfoHash)) { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found session credentials to use'); + + const { username, password } = this.sessionCredentials.get(authInfoHash)!; + return { username, password }; + } let storedUsername: string | undefined; let storedPassword: string | undefined; @@ -166,13 +214,32 @@ export class ProxyAuthHandler extends Disposable { // Reply with stored credentials unless we used them already. // In that case we need to show a login dialog again because // they seem invalid. - if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') { + if (authInfo.attempt === 1 && typeof storedUsername === 'string' && typeof storedPassword === 'string') { this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use'); - this.state = ProxyAuthState.StoredCredentialsUsed; + this.sessionCredentials.set(authInfoHash, { username: storedUsername, password: storedPassword }); return { username: storedUsername, password: storedPassword }; } + const previousDialog = this.currentDialog; + const currentDialog = this.currentDialog = (async () => { + await previousDialog; + const credentials = await this.showProxyCredentialsDialog(authInfo, authInfoHash, storedUsername, storedPassword); + if (this.currentDialog === currentDialog!) { + this.currentDialog = undefined; + } + return credentials; + })(); + return currentDialog; + } + + private async showProxyCredentialsDialog(authInfo: AuthInfo, authInfoHash: string, storedUsername: string | undefined, storedPassword: string | undefined): Promise { + if (this.cancelledAuthInfoHashes.has(authInfoHash)) { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - login dialog was cancelled before, not showing again'); + + return undefined; + } + // Find suitable window to show dialog: prefer to show it in the // active window because any other network request will wait on // the credentials and we want the user to present the dialog. @@ -186,14 +253,14 @@ export class ProxyAuthHandler extends Disposable { this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`); // Open proxy dialog + const sessionCredentials = this.sessionCredentials.get(authInfoHash); const payload = { authInfo, - username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored - password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored + username: sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored + password: sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` }; window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload); - this.state = ProxyAuthState.LoginDialogShown; // Handle reply const loginDialogCredentials = await new Promise(resolve => { @@ -229,6 +296,7 @@ export class ProxyAuthHandler extends Disposable { // We did not get any credentials from the window (e.g. cancelled) else { + this.cancelledAuthInfoHashes.add(authInfoHash); resolve(undefined); } } @@ -240,7 +308,7 @@ export class ProxyAuthHandler extends Disposable { // Remember credentials for the session in case // the credentials are wrong and we show the dialog // again - this.sessionCredentials = loginDialogCredentials; + this.sessionCredentials.set(authInfoHash, loginDialogCredentials); return loginDialogCredentials; } diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index f7235bd6834..e3013c06f8c 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { exec } from 'child_process'; import { app, BrowserWindow, clipboard, Display, Menu, MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, powerMonitor, SaveDialogOptions, SaveDialogReturnValue, screen, shell, webContents } from 'electron'; import { arch, cpus, freemem, loadavg, platform, release, totalmem, type } from 'os'; @@ -10,8 +11,8 @@ import { promisify } from 'util'; import { memoize } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import { dirname, join, resolve } from 'vs/base/common/path'; +import { matchesSomeScheme, Schemas } from 'vs/base/common/network'; +import { dirname, join, posix, resolve, win32 } from 'vs/base/common/path'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -43,6 +44,9 @@ import { IV8Profile } from 'vs/platform/profiling/common/profiling'; import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows'; import { IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow'; import { CancellationError } from 'vs/base/common/errors'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IProxyAuthService } from 'vs/platform/native/electron-main/auth'; +import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -61,7 +65,9 @@ export class NativeHostMainService extends Disposable implements INativeHostMain @ILogService private readonly logService: ILogService, @IProductService private readonly productService: IProductService, @IThemeMainService private readonly themeMainService: IThemeMainService, - @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProxyAuthService private readonly proxyAuthService: IProxyAuthService ) { super(); } @@ -322,7 +328,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } // Different source, delete it first - await Promises.unlink(source); + await fs.promises.unlink(source); } catch (error) { if (error.code !== 'ENOENT') { throw error; // throw on any error but file not found @@ -330,7 +336,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } try { - await Promises.symlink(target, source); + await fs.promises.symlink(target, source); } catch (error) { if (error.code !== 'EACCES' && error.code !== 'ENOENT') { throw error; @@ -362,7 +368,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const { source } = await this.getShellCommandLink(); try { - await Promises.unlink(source); + await fs.promises.unlink(source); } catch (error) { switch (error.code) { case 'EACCES': { @@ -485,14 +491,51 @@ export class NativeHostMainService extends Disposable implements INativeHostMain window?.setDocumentEdited(edited); } - async openExternal(windowId: number | undefined, url: string): Promise { + async openExternal(windowId: number | undefined, url: string, defaultApplication?: string): Promise { this.environmentMainService.unsetSnapExportedVariables(); - shell.openExternal(url); - this.environmentMainService.restoreSnapExportedVariables(); + try { + if (matchesSomeScheme(url, Schemas.http, Schemas.https)) { + this.openExternalBrowser(url, defaultApplication); + } else { + shell.openExternal(url); + } + } finally { + this.environmentMainService.restoreSnapExportedVariables(); + } return true; } + private async openExternalBrowser(url: string, defaultApplication?: string) { + const configuredBrowser = defaultApplication ?? this.configurationService.getValue('workbench.externalBrowser'); + if (!configuredBrowser) { + return shell.openExternal(url); + } + + if (configuredBrowser.includes(posix.sep) || configuredBrowser.includes(win32.sep)) { + const browserPathExists = await Promises.exists(configuredBrowser); + if (!browserPathExists) { + this.logService.error(`Configured external browser path does not exist: ${configuredBrowser}`); + return shell.openExternal(url); + } + } + + try { + const { default: open } = await import('open'); + await open(url, { + app: { + // Use `open.apps` helper to allow cross-platform browser + // aliases to be looked up properly. Fallback to the + // configured value if not found. + name: Object.hasOwn(open.apps, configuredBrowser) ? open.apps[(configuredBrowser as keyof typeof open['apps'])] : configuredBrowser + } + }); + } catch (error) { + this.logService.error(`Unable to open external URL '${url}' using browser '${configuredBrowser}' due to ${error}.`); + return shell.openExternal(url); + } + } + moveItemToTrash(windowId: number | undefined, fullPath: string): Promise { return shell.trashItem(fullPath); } @@ -500,7 +543,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async isAdmin(): Promise { let isAdmin: boolean; if (isWindows) { - isAdmin = (await import('native-is-elevated'))(); + isAdmin = (await import('native-is-elevated')).default(); } else { isAdmin = process.getuid?.() === 0; } @@ -615,6 +658,11 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region Process + async getProcessId(windowId: number | undefined): Promise { + const window = this.windowById(undefined, windowId); + return window?.win?.webContents.getOSProcessId(); + } + async killProcess(windowId: number | undefined, pid: number, code: string): Promise { process.kill(pid, code); } @@ -760,12 +808,22 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region Connectivity async resolveProxy(windowId: number | undefined, url: string): Promise { + if (this.environmentMainService.extensionTestsLocationURI) { + const testProxy = this.configurationService.getValue('integration-test.http.proxy'); + if (testProxy) { + return testProxy; + } + } const window = this.codeWindowById(windowId); const session = window?.win?.webContents?.session; return session?.resolveProxy(url); } + async lookupAuthorization(_windowId: number | undefined, authInfo: AuthInfo): Promise { + return this.proxyAuthService.lookupAuthorization(authInfo); + } + async loadCertificates(_windowId: number | undefined): Promise { const proxyAgent = await import('@vscode/proxy-agent'); return proxyAgent.loadSystemCertificates({ log: this.logService }); diff --git a/src/vs/platform/observable/common/platformObservableUtils.ts b/src/vs/platform/observable/common/platformObservableUtils.ts index 096993beb80..2e886ef6540 100644 --- a/src/vs/platform/observable/common/platformObservableUtils.ts +++ b/src/vs/platform/observable/common/platformObservableUtils.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable } from 'vs/base/common/lifecycle'; -import { autorunOpts, IObservable, IReader, observableFromEvent } from 'vs/base/common/observable'; +import { autorunOpts, IObservable, IReader } from 'vs/base/common/observable'; +import { observableFromEventOpts } from 'vs/base/common/observableInternal/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyValue, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; /** Creates an observable update when a configuration key updates. */ export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( + return observableFromEventOpts({ debugName: () => `Configuration Key "${key}"`, }, (handleChange) => configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(key)) { handleChange(e); diff --git a/src/vs/platform/observable/common/wrapInReloadableClass.ts b/src/vs/platform/observable/common/wrapInReloadableClass.ts new file mode 100644 index 00000000000..cfecf902c5f --- /dev/null +++ b/src/vs/platform/observable/common/wrapInReloadableClass.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { isHotReloadEnabled } from 'vs/base/common/hotReload'; +import { readHotReloadableExport } from 'vs/base/common/hotReloadHelpers'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { autorunWithStore } from 'vs/base/common/observable'; +import { BrandedService, GetLeadingNonServiceArgs, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +/** + * Wrap a class in a reloadable wrapper. + * When the wrapper is created, the original class is created. + * When the original class changes, the instance is re-created. +*/ +export function wrapInReloadableClass0(getClass: () => Result): Result> { + return !isHotReloadEnabled() ? getClass() : createWrapper(getClass, BaseClass0); +} + +type Result = new (...args: TArgs) => IDisposable; + +class BaseClass { + constructor( + public readonly instantiationService: IInstantiationService, + ) { } + + public init(...params: any[]): void { } +} + +function createWrapper(getClass: () => any, B: new (...args: T) => BaseClass) { + return (class ReloadableWrapper extends B { + private _autorun: IDisposable | undefined = undefined; + + override init(...params: any[]) { + this._autorun = autorunWithStore((reader, store) => { + const clazz = readHotReloadableExport(getClass(), reader); + store.add(this.instantiationService.createInstance(clazz as any, ...params) as IDisposable); + }); + } + + dispose(): void { + this._autorun?.dispose(); + } + }) as any; +} + +class BaseClass0 extends BaseClass { + constructor(@IInstantiationService i: IInstantiationService) { super(i); this.init(); } +} + +/** + * Wrap a class in a reloadable wrapper. + * When the wrapper is created, the original class is created. + * When the original class changes, the instance is re-created. +*/ +export function wrapInReloadableClass1(getClass: () => Result): Result> { + return !isHotReloadEnabled() ? getClass() as any : createWrapper(getClass, BaseClass1); +} + +class BaseClass1 extends BaseClass { + constructor(param1: any, @IInstantiationService i: IInstantiationService,) { super(i); this.init(param1); } +} diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index 710292ef17d..eb93b66a122 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -14,7 +14,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import 'vs/css!./link'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; export interface ILinkDescriptor { @@ -33,7 +33,7 @@ export interface ILinkOptions { export class Link extends Disposable { private el: HTMLAnchorElement; - private hover?: IUpdatableHover; + private hover?: IManagedHover; private hoverDelegate: IHoverDelegate; private _enabled: boolean = true; @@ -131,7 +131,7 @@ export class Link extends Disposable { if (this.hoverDelegate.showNativeHover) { this.el.title = title ?? ''; } else if (!this.hover && title) { - this.hover = this._register(this._hoverService.setupUpdatableHover(this.hoverDelegate, this.el, title)); + this.hover = this._register(this._hoverService.setupManagedHover(this.hoverDelegate, this.el, title)); } else if (this.hover) { this.hover.update(title); } diff --git a/src/vs/platform/opener/test/common/opener.test.ts b/src/vs/platform/opener/test/common/opener.test.ts index 35b6e027d66..93ee50f9901 100644 --- a/src/vs/platform/opener/test/common/opener.test.ts +++ b/src/vs/platform/opener/test/common/opener.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { extractSelection, withSelection } from 'vs/platform/opener/common/opener'; diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index eb383dadcb5..58278d978f9 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -53,12 +53,12 @@ else if (globalThis._VSCODE_PRODUCT_JSON && globalThis._VSCODE_PACKAGE_JSON) { else { // Built time configuration (do NOT modify) - product = { /*BUILD->INSERT_PRODUCT_CONFIGURATION*/ } as IProductConfiguration; + product = { /*BUILD->INSERT_PRODUCT_CONFIGURATION*/ } as any; // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.90.0-dev', + version: '1.91.0-dev', nameShort: 'Code - OSS Dev', nameLong: 'Code - OSS Dev', applicationName: 'code-oss', diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index e1eb0a2f5f2..2a1c7349aae 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -65,7 +65,7 @@ export interface IProgressNotificationOptions extends IProgressOptions { readonly secondaryActions?: readonly IAction[]; readonly delay?: number; readonly priority?: NotificationPriority; - readonly type?: 'syncing' | 'loading'; + readonly type?: 'loading' | 'syncing'; } export interface IProgressDialogOptions extends IProgressOptions { @@ -77,7 +77,7 @@ export interface IProgressDialogOptions extends IProgressOptions { export interface IProgressWindowOptions extends IProgressOptions { readonly location: ProgressLocation.Window; readonly command?: string; - readonly type?: 'syncing' | 'loading'; + readonly type?: 'loading' | 'syncing'; } export interface IProgressCompositeOptions extends IProgressOptions { diff --git a/src/vs/platform/progress/test/common/progress.test.ts b/src/vs/platform/progress/test/common/progress.test.ts index 85bce306781..24c2ddb78de 100644 --- a/src/vs/platform/progress/test/common/progress.test.ts +++ b/src/vs/platform/progress/test/common/progress.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AsyncProgress } from 'vs/platform/progress/common/progress'; diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 7b43505fb64..74414a631a1 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -206,7 +206,7 @@ abstract class QuickInput extends Disposable implements IQuickInput { } set widget(widget: unknown | undefined) { - if (!(widget instanceof HTMLElement)) { + if (!(dom.isHTMLElement(widget))) { return; } if (this._widget !== widget) { @@ -1036,9 +1036,6 @@ export class QuickPick extends QuickInput implements I // We want focus to exist in the list if there are items so that space can be used to toggle this.ui.list.shouldLoop = !this.canSelectMany; this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); - this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); - this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); - this.ui.count.setCount(this.ui.list.getCheckedCount()); switch (this._itemActivation) { case ItemActivation.NONE: this._itemActivation = ItemActivation.FIRST; // only valid once, then unset @@ -1271,7 +1268,7 @@ export class QuickInputHoverDelegate extends WorkbenchHoverDelegate { private getOverrideOptions(options: IHoverDelegateOptions): Partial { // Only show the hover hint if the content is of a decent size const showHoverHint = ( - options.content instanceof HTMLElement + dom.isHTMLElement(options.content) ? options.content.textContent ?? '' : typeof options.content === 'string' ? options.content diff --git a/src/vs/platform/quickinput/browser/quickInputActions.ts b/src/vs/platform/quickinput/browser/quickInputActions.ts index 14f4a49b539..5efaf793645 100644 --- a/src/vs/platform/quickinput/browser/quickInputActions.ts +++ b/src/vs/platform/quickinput/browser/quickInputActions.ts @@ -27,12 +27,13 @@ function registerQuickPickCommandAndKeybindingRule(rule: PartialExcept { if (!this.getUI().ignoreFocusOut && !this.options.ignoreFocusOut()) { @@ -275,7 +275,7 @@ export class QuickInputController extends Disposable { } else { selectors.push('input[type=text]'); } - if (this.getUI().list.isDisplayed()) { + if (this.getUI().list.displayed) { selectors.push('.monaco-list'); } // focus links if there are any @@ -580,7 +580,6 @@ export class QuickInputController extends Disposable { ui.count.setCount(0); dom.reset(ui.message); ui.progressBar.stop(); - ui.list.setElements([]); ui.list.matchOnDescription = false; ui.list.matchOnDetail = false; ui.list.matchOnLabel = true; @@ -615,7 +614,7 @@ export class QuickInputController extends Disposable { ui.customButtonContainer.style.display = visibilities.customButton ? '' : 'none'; ui.message.style.display = visibilities.message ? '' : 'none'; ui.progressBar.getContainer().style.display = visibilities.progressBar ? '' : 'none'; - ui.list.display(!!visibilities.list); + ui.list.displayed = !!visibilities.list; ui.container.classList.toggle('show-checkboxes', !!visibilities.checkBox); ui.container.classList.toggle('hidden-input', !visibilities.inputBox && !visibilities.description); this.updateLayout(); // TODO @@ -684,7 +683,7 @@ export class QuickInputController extends Disposable { } navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration) { - if (this.isVisible() && this.getUI().list.isDisplayed()) { + if (this.isVisible() && this.getUI().list.displayed) { this.getUI().list.focus(next ? QuickPickFocus.Next : QuickPickFocus.Previous); if (quickNavigate && this.controller instanceof QuickPick) { this.controller.quickNavigate = quickNavigate; diff --git a/src/vs/platform/quickinput/browser/quickInputTree.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts index 3a492f78a1e..83976c15065 100644 --- a/src/vs/platform/quickinput/browser/quickInputTree.ts +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { Emitter, Event, IValueWithChangeEvent } from 'vs/base/common/event'; +import { Emitter, Event, EventBufferer, IValueWithChangeEvent } from 'vs/base/common/event'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IObjectTreeElement, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; @@ -36,9 +36,11 @@ import { ltrim } from 'vs/base/common/strings'; import { RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; import { ThrottledDelayer } from 'vs/base/common/async'; import { isCancellationError } from 'vs/base/common/errors'; -import type { IHoverWidget, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IHoverWidget, IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { QuickPickFocus } from '../common/quickInput'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { observableValue, observableValueOpts } from 'vs/base/common/observable'; +import { equals } from 'vs/base/common/arrays'; const $ = dom.$; @@ -434,7 +436,7 @@ class QuickPickItemElementRenderer extends BaseQuickInputListRenderer(); /** * Event that is fired when the tree receives a keydown. @@ -668,17 +672,17 @@ export class QuickInputTree extends Disposable { */ readonly onLeave: Event = this._onLeave.event; - private readonly _onChangedAllVisibleChecked = new Emitter(); - onChangedAllVisibleChecked: Event = this._onChangedAllVisibleChecked.event; + private readonly _visibleCountObservable = observableValue('VisibleCount', 0); + onChangedVisibleCount: Event = Event.fromObservable(this._visibleCountObservable, this._store); - private readonly _onChangedCheckedCount = new Emitter(); - onChangedCheckedCount: Event = this._onChangedCheckedCount.event; + private readonly _allVisibleCheckedObservable = observableValue('AllVisibleChecked', false); + onChangedAllVisibleChecked: Event = Event.fromObservable(this._allVisibleCheckedObservable, this._store); - private readonly _onChangedVisibleCount = new Emitter(); - onChangedVisibleCount: Event = this._onChangedVisibleCount.event; + private readonly _checkedCountObservable = observableValue('CheckedCount', 0); + onChangedCheckedCount: Event = Event.fromObservable(this._checkedCountObservable, this._store); - private readonly _onChangedCheckedElements = new Emitter(); - onChangedCheckedElements: Event = this._onChangedCheckedElements.event; + private readonly _checkedElementsObservable = observableValueOpts({ equalsFn: equals }, new Array()); + onChangedCheckedElements: Event = Event.fromObservable(this._checkedElementsObservable, this._store); private readonly _onButtonTriggered = new Emitter>(); onButtonTriggered = this._onButtonTriggered.event; @@ -686,21 +690,23 @@ export class QuickInputTree extends Disposable { private readonly _onSeparatorButtonTriggered = new Emitter(); onSeparatorButtonTriggered = this._onSeparatorButtonTriggered.event; + private readonly _elementChecked = new Emitter<{ element: IQuickPickElement; checked: boolean }>(); + private readonly _elementCheckedEventBufferer = new EventBufferer(); + + //#endregion + + private _hasCheckboxes = false; + private readonly _container: HTMLElement; private readonly _tree: WorkbenchObjectTree; private readonly _separatorRenderer: QuickPickSeparatorElementRenderer; private readonly _itemRenderer: QuickPickItemElementRenderer; - private readonly _elementChecked = new Emitter<{ element: IQuickPickElement; checked: boolean }>(); private _inputElements = new Array(); private _elementTree = new Array(); private _itemElements = new Array(); // Elements that apply to the current set of elements private readonly _elementDisposable = this._register(new DisposableStore()); private _lastHover: IHoverWidget | undefined; - // This is used to prevent setting the checked state of a single element from firing the checked events - // so that we can batch them together. This can probably be improved by handling events differently, - // but this works for now. An observable would probably be ideal for this. - private _shouldFireCheckedEvents = true; constructor( private parent: HTMLElement, @@ -743,7 +749,8 @@ export class QuickInputTree extends Disposable { get onDidChangeFocus() { return Event.map( this._tree.onDidChangeFocus, - e => e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement).map(e => e.item) + e => e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement).map(e => e.item), + this._store ); } @@ -754,10 +761,19 @@ export class QuickInputTree extends Disposable { e => ({ items: e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement).map(e => e.item), event: e.browserEvent - }) + }), + this._store ); } + get displayed() { + return this._container.style.display !== 'none'; + } + + set displayed(value: boolean) { + this._container.style.display = value ? '' : 'none'; + } + get scrollTop() { return this._tree.scrollTop; } @@ -842,6 +858,7 @@ export class QuickInputTree extends Disposable { this._registerOnKeyDown(); this._registerOnContainerClick(); this._registerOnMouseMiddleClick(); + this._registerOnTreeModelChanged(); this._registerOnElementChecked(); this._registerOnContextMenu(); this._registerHoverListeners(); @@ -879,8 +896,19 @@ export class QuickInputTree extends Disposable { })); } + private _registerOnTreeModelChanged() { + this._register(this._tree.onDidChangeModel(() => { + const visibleCount = this._itemElements.filter(e => !e.hidden).length; + this._visibleCountObservable.set(visibleCount, undefined); + if (this._hasCheckboxes) { + this._updateCheckedObservables(); + } + })); + } + private _registerOnElementChecked() { - this._register(this._elementChecked.event(_ => this._fireCheckedEvents())); + // Only fire the last event when buffered + this._register(this._elementCheckedEventBufferer.wrapEvent(this._elementChecked.event, (_, e) => e)(_ => this._updateCheckedObservables())); } private _registerOnContextMenu() { @@ -903,13 +931,13 @@ export class QuickInputTree extends Disposable { this._register(this._tree.onMouseOver(async e => { // If we hover over an anchor element, we don't want to show the hover because // the anchor may have a tooltip that we want to show instead. - if (e.browserEvent.target instanceof HTMLAnchorElement) { + if (dom.isHTMLAnchorElement(e.browserEvent.target)) { delayer.cancel(); return; } if ( // anchors are an exception as called out above so we skip them here - !(e.browserEvent.relatedTarget instanceof HTMLAnchorElement) && + !(dom.isHTMLAnchorElement(e.browserEvent.relatedTarget)) && // check if the mouse is still over the same element dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node) ) { @@ -1015,37 +1043,21 @@ export class QuickInputTree extends Disposable { //#region public methods - getAllVisibleChecked() { - return this._allVisibleChecked(this._itemElements, false); - } - - getCheckedCount() { - return this._itemElements.filter(element => element.checked).length; - } - - getVisibleCount() { - return this._itemElements.filter(e => !e.hidden).length; - } - setAllVisibleChecked(checked: boolean) { - try { - this._shouldFireCheckedEvents = false; + this._elementCheckedEventBufferer.bufferEvents(() => { this._itemElements.forEach(element => { if (!element.hidden && !element.checkboxDisabled) { - // Would fire an event if we didn't have the flag set + // Would fire an event if we didn't beffer the events element.checked = checked; } }); - } finally { - this._shouldFireCheckedEvents = true; - this._fireCheckedEvents(); - } + }); } setElements(inputElements: QuickPickItem[]): void { this._elementDisposable.clear(); this._inputElements = inputElements; - const hasCheckbox = this.parent.classList.contains('show-checkboxes'); + this._hasCheckboxes = this.parent.classList.contains('show-checkboxes'); let currentSeparatorElement: QuickPickSeparatorElement | undefined; this._itemElements = new Array(); this._elementTree = inputElements.reduce((result, item, index) => { @@ -1057,7 +1069,7 @@ export class QuickInputTree extends Disposable { } currentSeparatorElement = new QuickPickSeparatorElement( index, - (event: IQuickPickSeparatorButtonEvent) => this.fireSeparatorButtonTriggered(event), + e => this._onSeparatorButtonTriggered.fire(e), item ); element = currentSeparatorElement; @@ -1071,8 +1083,8 @@ export class QuickInputTree extends Disposable { } const qpi = new QuickPickItemElement( index, - hasCheckbox, - (event: IQuickPickItemButtonEvent) => this.fireButtonTriggered(event), + this._hasCheckboxes, + e => this._onButtonTriggered.fire(e), this._elementChecked, item, separator, @@ -1090,32 +1102,7 @@ export class QuickInputTree extends Disposable { return result; }, new Array()); - const elements = new Array>(); - let visibleCount = 0; - for (const element of this._elementTree) { - if (element instanceof QuickPickSeparatorElement) { - elements.push({ - element, - collapsible: false, - collapsed: false, - children: element.children.map(e => ({ - element: e, - collapsible: false, - collapsed: false, - })), - }); - visibleCount += element.children.length + 1; // +1 for the separator itself; - } else { - elements.push({ - element, - collapsible: false, - collapsed: false, - }); - visibleCount++; - } - } - this._tree.setChildren(null, elements); - this._onChangedVisibleCount.fire(visibleCount); + this._setElementsToTree(this._elementTree); // Accessibility hack, unfortunately on next tick // https://github.com/microsoft/vscode/issues/211976 @@ -1125,24 +1112,13 @@ export class QuickInputTree extends Disposable { const parent = focusedElement?.parentNode; if (focusedElement && parent) { const nextSibling = focusedElement.nextSibling; - parent.removeChild(focusedElement); + focusedElement.remove(); parent.insertBefore(focusedElement, nextSibling); } }, 0); } } - getElementsCount(): number { - return this._inputElements.length; - } - - getFocusedElements() { - return this._tree.getFocus() - .filter((e): e is IQuickPickElement => !!e) - .map(e => e.item) - .filter((e): e is IQuickPickItem => !!e); - } - setFocusedElements(items: IQuickPickItem[]) { const elements = items.map(item => this._itemElements.find(e => e.item === item)) .filter((e): e is QuickPickItemElement => !!e); @@ -1159,12 +1135,6 @@ export class QuickInputTree extends Disposable { return this._tree.getHTMLElement().getAttribute('aria-activedescendant'); } - getSelectedElements() { - return this._tree.getSelection() - .filter((e): e is IQuickPickElement => !!e && !!(e as QuickPickItemElement).item) - .map(e => e.item); - } - setSelectedElements(items: IQuickPickItem[]) { const elements = items.map(item => this._itemElements.find(e => e.item === item)) .filter((e): e is QuickPickItemElement => !!e); @@ -1177,20 +1147,16 @@ export class QuickInputTree extends Disposable { } setCheckedElements(items: IQuickPickItem[]) { - try { - this._shouldFireCheckedEvents = false; + this._elementCheckedEventBufferer.bufferEvents(() => { const checked = new Set(); for (const item of items) { checked.add(item); } for (const element of this._itemElements) { - // Would fire an event if we didn't have the flag set + // Would fire an event if we didn't beffer the events element.checked = checked.has(element.item); } - } finally { - this._shouldFireCheckedEvents = true; - this._fireCheckedEvents(); - } + }); } focus(what: QuickPickFocus): void { @@ -1207,13 +1173,24 @@ export class QuickInputTree extends Disposable { this._tree.scrollTop = 0; this._tree.focusFirst(undefined, (e) => e.element instanceof QuickPickItemElement); break; - case QuickPickFocus.Second: + case QuickPickFocus.Second: { this._tree.scrollTop = 0; - this._tree.setFocus([this._itemElements[1]]); + let isSecondItem = false; + this._tree.focusFirst(undefined, (e) => { + if (!(e.element instanceof QuickPickItemElement)) { + return false; + } + if (isSecondItem) { + return true; + } + isSecondItem = !isSecondItem; + return false; + }); break; + } case QuickPickFocus.Last: this._tree.scrollTop = this._tree.scrollHeight; - this._tree.setFocus([this._itemElements[this._itemElements.length - 1]]); + this._tree.focusLast(undefined, (e) => e.element instanceof QuickPickItemElement); break; case QuickPickFocus.Next: { const prevFocus = this._tree.getFocus(); @@ -1315,7 +1292,7 @@ export class QuickInputTree extends Disposable { // If we didn't move, then we should just move to the end // of the list. this._tree.scrollTop = this._tree.scrollHeight; - this._tree.setFocus([this._itemElements[this._itemElements.length - 1]]); + this._tree.focusLast(undefined, (e) => e.element instanceof QuickPickItemElement); } break; } @@ -1477,39 +1454,13 @@ export class QuickInputTree extends Disposable { return result; }, new Array()); - const elements = new Array>(); - for (const element of finalElements) { - if (element instanceof QuickPickSeparatorElement) { - elements.push({ - element, - collapsible: false, - collapsed: false, - children: element.children.map(e => ({ - element: e, - collapsible: false, - collapsed: false, - })), - }); - } else { - elements.push({ - element, - collapsible: false, - collapsed: false, - }); - } - } - this._tree.setChildren(null, elements); + this._setElementsToTree(finalElements); this._tree.layout(); - - this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); - this._onChangedVisibleCount.fire(shownElements.length); - return true; } toggleCheckbox() { - try { - this._shouldFireCheckedEvents = false; + this._elementCheckedEventBufferer.bufferEvents(() => { const elements = this._tree.getFocus().filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement); const allChecked = this._allVisibleChecked(elements); for (const element of elements) { @@ -1518,18 +1469,7 @@ export class QuickInputTree extends Disposable { element.checked = !allChecked; } } - } finally { - this._shouldFireCheckedEvents = true; - this._fireCheckedEvents(); - } - } - - display(display: boolean) { - this._container.style.display = display ? '' : 'none'; - } - - isDisplayed() { - return this._container.style.display !== 'none'; + }); } style(styles: IListStyles) { @@ -1566,6 +1506,31 @@ export class QuickInputTree extends Disposable { //#region private methods + private _setElementsToTree(elements: IQuickPickElement[]) { + const treeElements = new Array>(); + for (const element of elements) { + if (element instanceof QuickPickSeparatorElement) { + treeElements.push({ + element, + collapsible: false, + collapsed: false, + children: element.children.map(e => ({ + element: e, + collapsible: false, + collapsed: false, + })), + }); + } else { + treeElements.push({ + element, + collapsible: false, + collapsed: false, + }); + } + } + this._tree.setChildren(null, treeElements); + } + private _allVisibleChecked(elements: QuickPickItemElement[], whenNoneVisible = true) { for (let i = 0, n = elements.length; i < n; i++) { const element = elements[i]; @@ -1580,21 +1545,11 @@ export class QuickInputTree extends Disposable { return whenNoneVisible; } - private _fireCheckedEvents() { - if (!this._shouldFireCheckedEvents) { - return; - } - this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); - this._onChangedCheckedCount.fire(this.getCheckedCount()); - this._onChangedCheckedElements.fire(this.getCheckedElements()); - } - - private fireButtonTriggered(event: IQuickPickItemButtonEvent) { - this._onButtonTriggered.fire(event); - } - - private fireSeparatorButtonTriggered(event: IQuickPickSeparatorButtonEvent) { - this._onSeparatorButtonTriggered.fire(event); + private _updateCheckedObservables() { + this._allVisibleCheckedObservable.set(this._allVisibleChecked(this._itemElements, false), undefined); + const checkedCount = this._itemElements.filter(element => element.checked).length; + this._checkedCountObservable.set(checkedCount, undefined); + this._checkedElementsObservable.set(this.getCheckedElements(), undefined); } /** diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index 4fd3faf30e0..12b7afa785e 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -30,7 +30,6 @@ export interface IQuickAccessProviderRunOptions { export interface AnythingQuickAccessProviderRunOptions extends IQuickAccessProviderRunOptions { readonly includeHelp?: boolean; readonly filter?: (item: unknown) => boolean; - readonly includeSymbols?: boolean; /** * @deprecated - temporary for Dynamic Chat Variables (see usage) until it has built-in UX for file picking * Useful for adding items to the top of the list that might contain actions. diff --git a/src/vs/platform/quickinput/test/browser/quickinput.test.ts b/src/vs/platform/quickinput/test/browser/quickinput.test.ts index 8e8fc694be0..328c43d72fb 100644 --- a/src/vs/platform/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/platform/quickinput/test/browser/quickinput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { unthemedInboxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; import { unthemedListStyles } from 'vs/base/browser/ui/list/listWidget'; @@ -57,7 +57,7 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 setup(() => { const fixture = document.createElement('div'); mainWindow.document.body.appendChild(fixture); - store.add(toDisposable(() => mainWindow.document.body.removeChild(fixture))); + store.add(toDisposable(() => fixture.remove())); const instantiationService = new TestInstantiationService(); diff --git a/src/vs/platform/registry/test/common/platform.test.ts b/src/vs/platform/registry/test/common/platform.test.ts index 3fe9188fa8d..8ec965d503b 100644 --- a/src/vs/platform/registry/test/common/platform.test.ts +++ b/src/vs/platform/registry/test/common/platform.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isFunction } from 'vs/base/common/types'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/platform/remote/common/remoteExtensionsScanner.ts b/src/vs/platform/remote/common/remoteExtensionsScanner.ts index 792c0352bb7..a26de9d171b 100644 --- a/src/vs/platform/remote/common/remoteExtensionsScanner.ts +++ b/src/vs/platform/remote/common/remoteExtensionsScanner.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -16,5 +15,4 @@ export interface IRemoteExtensionsScannerService { whenExtensionsReady(): Promise; scanExtensions(): Promise; - scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise; } diff --git a/src/vs/platform/remote/node/wsl.ts b/src/vs/platform/remote/node/wsl.ts index 3ba33f74b96..4bc71a35f0c 100644 --- a/src/vs/platform/remote/node/wsl.ts +++ b/src/vs/platform/remote/node/wsl.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as os from 'os'; import * as cp from 'child_process'; -import { Promises } from 'vs/base/node/pfs'; import * as path from 'path'; let hasWSLFeaturePromise: Promise | undefined; @@ -33,7 +33,7 @@ async function testWSLFeatureInstalled(): Promise { const dllPath = getLxssManagerDllPath(); if (dllPath) { try { - if ((await Promises.stat(dllPath)).isFile()) { + if ((await fs.promises.stat(dllPath)).isFile()) { return true; } } catch (e) { diff --git a/src/vs/platform/remote/test/common/remoteHosts.test.ts b/src/vs/platform/remote/test/common/remoteHosts.test.ts index 0f2254b8b87..ed564551df9 100644 --- a/src/vs/platform/remote/test/common/remoteHosts.test.ts +++ b/src/vs/platform/remote/test/common/remoteHosts.test.ts @@ -3,11 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseAuthorityWithOptionalPort, parseAuthorityWithPort } from 'vs/platform/remote/common/remoteHosts'; suite('remoteHosts', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('parseAuthority hostname', () => { assert.deepStrictEqual(parseAuthorityWithPort('localhost:8080'), { host: 'localhost', port: 8080 }); }); diff --git a/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts b/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts index a5bacbb8c86..67790a807c6 100644 --- a/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts +++ b/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; diff --git a/src/vs/platform/request/browser/requestService.ts b/src/vs/platform/request/browser/requestService.ts index f0fc3d68790..70f4e65bcbc 100644 --- a/src/vs/platform/request/browser/requestService.ts +++ b/src/vs/platform/request/browser/requestService.ts @@ -8,7 +8,7 @@ import { request } from 'vs/base/parts/request/browser/request'; import { IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILoggerService } from 'vs/platform/log/common/log'; -import { AbstractRequestService, IRequestService } from 'vs/platform/request/common/request'; +import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; /** * This service exposes the `request` API, while using the global @@ -36,6 +36,10 @@ export class RequestService extends AbstractRequestService implements IRequestSe return undefined; // not implemented in the web } + async lookupAuthorization(authInfo: AuthInfo): Promise { + return undefined; // not implemented in the web + } + async loadCertificates(): Promise { return []; // not implemented in the web } diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 289ad6740e0..e8574d11f7f 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -16,12 +16,27 @@ import { Registry } from 'vs/platform/registry/common/platform'; export const IRequestService = createDecorator('requestService'); +export interface AuthInfo { + isProxy: boolean; + scheme: string; + host: string; + port: number; + realm: string; + attempt: number; +} + +export interface Credentials { + username: string; + password: string; +} + export interface IRequestService { readonly _serviceBrand: undefined; request(options: IRequestOptions, token: CancellationToken): Promise; resolveProxy(url: string): Promise; + lookupAuthorization(authInfo: AuthInfo): Promise; loadCertificates(): Promise; } @@ -80,6 +95,7 @@ export abstract class AbstractRequestService extends Disposable implements IRequ abstract request(options: IRequestOptions, token: CancellationToken): Promise; abstract resolveProxy(url: string): Promise; + abstract lookupAuthorization(authInfo: AuthInfo): Promise; abstract loadCertificates(): Promise; } @@ -155,6 +171,12 @@ function registerProxyConfigurations(scope: ConfigurationScope): void { markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set."), restricted: true }, + 'http.noProxy': { + type: 'array', + items: { type: 'string' }, + markdownDescription: localize('noProxy', "Specifies domain names for which proxy settings should be ignored for HTTP/HTTPS requests."), + restricted: true + }, 'http.proxyAuthorization': { type: ['null', 'string'], default: null, diff --git a/src/vs/platform/request/common/requestIpc.ts b/src/vs/platform/request/common/requestIpc.ts index 421106ebff6..b489a52dacc 100644 --- a/src/vs/platform/request/common/requestIpc.ts +++ b/src/vs/platform/request/common/requestIpc.ts @@ -8,7 +8,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; -import { IRequestService } from 'vs/platform/request/common/request'; +import { AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; type RequestResponse = [ { @@ -34,6 +34,7 @@ export class RequestChannel implements IServerChannel { return [{ statusCode: res.statusCode, headers: res.headers }, buffer]; }); case 'resolveProxy': return this.service.resolveProxy(args[0]); + case 'lookupAuthorization': return this.service.lookupAuthorization(args[0]); case 'loadCertificates': return this.service.loadCertificates(); } throw new Error('Invalid call'); @@ -55,6 +56,10 @@ export class RequestChannelClient implements IRequestService { return this.channel.call('resolveProxy', [url]); } + async lookupAuthorization(authInfo: AuthInfo): Promise { + return this.channel.call<{ username: string; password: string } | undefined>('lookupAuthorization', [authInfo]); + } + async loadCertificates(): Promise { return this.channel.call('loadCertificates'); } diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 23f8f0d44c8..dd2165c1ce3 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -17,7 +17,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; import { ILogService, ILoggerService } from 'vs/platform/log/common/log'; -import { AbstractRequestService, IRequestService } from 'vs/platform/request/common/request'; +import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; import { Agent, getProxyAgent } from 'vs/platform/request/node/proxy'; import { createGunzip } from 'zlib'; @@ -110,6 +110,10 @@ export class RequestService extends AbstractRequestService implements IRequestSe return undefined; // currently not implemented in node } + async lookupAuthorization(authInfo: AuthInfo): Promise { + return undefined; // currently not implemented in node + } + async loadCertificates(): Promise { const proxyAgent = await import('@vscode/proxy-agent'); return proxyAgent.loadSystemCertificates({ log: this.logService }); diff --git a/src/vs/platform/secrets/test/common/secrets.test.ts b/src/vs/platform/secrets/test/common/secrets.test.ts index b3a048af2f2..50def3a9a92 100644 --- a/src/vs/platform/secrets/test/common/secrets.test.ts +++ b/src/vs/platform/secrets/test/common/secrets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEncryptionService, KnownStorageProvider } from 'vs/platform/encryption/common/encryptionService'; diff --git a/src/vs/platform/state/test/node/state.test.ts b/src/vs/platform/state/test/node/state.test.ts index 493d78d0e51..4674f20a4ee 100644 --- a/src/vs/platform/state/test/node/state.test.ts +++ b/src/vs/platform/state/test/node/state.test.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { readFileSync } from 'fs'; +import assert from 'assert'; +import { readFileSync, promises } from 'fs'; import { tmpdir } from 'os'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; @@ -37,7 +37,7 @@ flakySuite('StateService', () => { diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(logService)); disposables.add(fileService.registerProvider(Schemas.file, diskFileSystemProvider)); - return Promises.mkdir(testDir, { recursive: true }); + return promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts index c110f28015b..8d6d1b539d5 100644 --- a/src/vs/platform/storage/electron-main/storageMain.ts +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { top } from 'vs/base/common/arrays'; import { DeferredPromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -403,7 +404,7 @@ export class WorkspaceStorageMain extends BaseStorageMain { } // Ensure storage folder exists - await Promises.mkdir(workspaceStorageFolderPath, { recursive: true }); + await fs.promises.mkdir(workspaceStorageFolderPath, { recursive: true }); // Write metadata into folder (but do not await) this.ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath); diff --git a/src/vs/platform/telemetry/node/telemetry.ts b/src/vs/platform/telemetry/node/telemetry.ts index 72f87dddf35..d1770958d3b 100644 --- a/src/vs/platform/telemetry/node/telemetry.ts +++ b/src/vs/platform/telemetry/node/telemetry.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { join } from 'vs/base/common/path'; import { Promises } from 'vs/base/node/pfs'; @@ -21,7 +22,7 @@ export async function buildTelemetryMessage(appRoot: string, extensionsPath?: st const files = await Promises.readdir(extensionsPath); for (const file of files) { try { - const fileStat = await Promises.stat(join(extensionsPath, file)); + const fileStat = await fs.promises.stat(join(extensionsPath, file)); if (fileStat.isDirectory()) { dirs.push(file); } @@ -39,15 +40,15 @@ export async function buildTelemetryMessage(appRoot: string, extensionsPath?: st } for (const folder of telemetryJsonFolders) { - const contents = (await Promises.readFile(join(extensionsPath, folder, 'telemetry.json'))).toString(); + const contents = (await fs.promises.readFile(join(extensionsPath, folder, 'telemetry.json'))).toString(); mergeTelemetry(contents, folder); } } - let contents = (await Promises.readFile(join(appRoot, 'telemetry-core.json'))).toString(); + let contents = (await fs.promises.readFile(join(appRoot, 'telemetry-core.json'))).toString(); mergeTelemetry(contents, 'vscode-core'); - contents = (await Promises.readFile(join(appRoot, 'telemetry-extensions.json'))).toString(); + contents = (await fs.promises.readFile(join(appRoot, 'telemetry-extensions.json'))).toString(); mergeTelemetry(contents, 'vscode-extensions'); return JSON.stringify(mergedTelemetry, null, 4); diff --git a/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts b/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts index 2ee6f9bc99b..33a22c391b2 100644 --- a/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts +++ b/src/vs/platform/telemetry/test/browser/1dsAppender.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type { ITelemetryItem, ITelemetryUnloadState } from '@microsoft/1ds-core-js'; -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { OneDataSystemWebAppender } from 'vs/platform/telemetry/browser/1dsAppender'; import { IAppInsightsCore } from 'vs/platform/telemetry/common/1dsAppender'; diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index 8524e9a6996..bc75667e949 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; -import * as sinonTest from 'sinon-test'; +import sinonTest from 'sinon-test'; import { mainWindow } from 'vs/base/browser/window'; import * as Errors from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts b/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts index 8de29c1eeeb..2fe9b7a6477 100644 --- a/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts +++ b/src/vs/platform/telemetry/test/common/telemetryLogAppender.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 426de4a8ff7..5cd3d9be955 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import type { IPromptInputModel } from 'vs/platform/terminal/common/capabilities/commandDetection/promptInputModel'; +import type { IPromptInputModel, ISerializedPromptInputModel } from 'vs/platform/terminal/common/capabilities/commandDetection/promptInputModel'; import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; import { ITerminalOutputMatch, ITerminalOutputMatcher } from 'vs/platform/terminal/common/terminal'; import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess'; @@ -169,11 +169,6 @@ export interface ICommandDetectionCapability { readonly executingCommandObject: ITerminalCommand | undefined; /** The current cwd at the cursor's position. */ readonly cwd: string | undefined; - /** - * Whether a command is currently being input. If the a command is current not being input or - * the state cannot reliably be detected the fallback of undefined will be used. - */ - readonly hasInput: boolean | undefined; readonly currentCommand: ICurrentPartialCommand | undefined; readonly onCommandStarted: Event; readonly onCommandFinished: Event; @@ -306,6 +301,7 @@ export interface IMarkProperties { export interface ISerializedCommandDetectionCapability { isWindowsPty: boolean; commands: ISerializedTerminalCommand[]; + promptInputModel: ISerializedPromptInputModel | undefined; } export interface IPtyHostProcessReplayEvent { events: ReplayEntry[]; diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts index 0f1c53efded..dbce8f54458 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts @@ -49,6 +49,14 @@ export interface IPromptInputModelState { readonly ghostTextIndex: number; } +export interface ISerializedPromptInputModel { + readonly modelState: IPromptInputModelState; + readonly commandStartX: number; + readonly lastPromptLine: string | undefined; + readonly continuationPrompt: string | undefined; + readonly lastUserInput: string; +} + export class PromptInputModel extends Disposable implements IPromptInputModel { private _state: PromptInputState = PromptInputState.Unknown; @@ -142,6 +150,26 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { return result; } + serialize(): ISerializedPromptInputModel { + return { + modelState: this._createStateObject(), + commandStartX: this._commandStartX, + lastPromptLine: this._lastPromptLine, + continuationPrompt: this._continuationPrompt, + lastUserInput: this._lastUserInput + }; + } + + deserialize(serialized: ISerializedPromptInputModel): void { + this._value = serialized.modelState.value; + this._cursorIndex = serialized.modelState.cursorIndex; + this._ghostTextIndex = serialized.modelState.ghostTextIndex; + this._commandStartX = serialized.commandStartX; + this._lastPromptLine = serialized.lastPromptLine; + this._continuationPrompt = serialized.continuationPrompt; + this._lastUserInput = serialized.lastUserInput; + } + private _handleCommandStart(command: { marker: IMarker }) { if (this._state === PromptInputState.Input) { return; @@ -220,8 +248,13 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { const absoluteCursorY = buffer.baseY + buffer.cursorY; let value = commandLine; - let cursorIndex = absoluteCursorY === commandStartY ? this._getRelativeCursorIndex(this._commandStartX, buffer, line) : commandLine.trimEnd().length + 1; let ghostTextIndex = -1; + let cursorIndex: number; + if (absoluteCursorY === commandStartY) { + cursorIndex = this._getRelativeCursorIndex(this._commandStartX, buffer, line); + } else { + cursorIndex = commandLine.trimEnd().length; + } // Detect ghost text by looking for italic or dim text in or after the cursor and // non-italic/dim text in the cell closest non-whitespace cell before the cursor @@ -235,15 +268,25 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { line = buffer.getLine(y); const lineText = line?.translateToString(true); if (lineText && line) { + // Check if the line wrapped without a new line (continuation) + if (line.isWrapped) { + value += lineText; + const relativeCursorIndex = this._getRelativeCursorIndex(0, buffer, line); + if (absoluteCursorY === y) { + cursorIndex += relativeCursorIndex; + } else { + cursorIndex += lineText.length; + } + } // Verify continuation prompt if we have it, if this line doesn't have it then the - // user likely just pressed enter - if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) { + // user likely just pressed enter. + else if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) { const trimmedLineText = this._trimContinuationPrompt(lineText); value += `\n${trimmedLineText}`; if (absoluteCursorY === y) { const continuationCellWidth = this._getContinuationPromptCellWidth(line, lineText); const relativeCursorIndex = this._getRelativeCursorIndex(continuationCellWidth, buffer, line); - cursorIndex += relativeCursorIndex; + cursorIndex += relativeCursorIndex + 1; } else { cursorIndex += trimmedLineText.length + 1; } diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 8df3d3b8f42..bef121fbff5 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -56,23 +56,6 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe } get cwd(): string | undefined { return this._cwd; } get promptTerminator(): string | undefined { return this._promptTerminator; } - private get _isInputting(): boolean { - return !!(this._currentCommand.commandStartMarker && !this._currentCommand.commandExecutedMarker); - } - - get hasInput(): boolean | undefined { - if (!this._isInputting || !this._currentCommand?.commandStartMarker) { - return undefined; - } - if (this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY === this._currentCommand.commandStartMarker?.line) { - const line = this._terminal.buffer.active.getLine(this._terminal.buffer.active.cursorY)?.translateToString(true, this._currentCommand.commandStartX); - if (line === undefined) { - return undefined; - } - return line.length > 0; - } - return true; - } private readonly _onCommandStarted = this._register(new Emitter()); readonly onCommandStarted = this._onCommandStarted.event; @@ -425,7 +408,8 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe } return { isWindowsPty: this._ptyHeuristics.value instanceof WindowsPtyHeuristics, - commands + commands, + promptInputModel: this._promptInputModel.serialize(), }; } @@ -460,6 +444,9 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand); this._onCommandFinished.fire(newCommand); } + if (serialized.promptInputModel) { + this._promptInputModel.deserialize(serialized.promptInputModel); + } } } diff --git a/src/vs/platform/terminal/common/terminalRecorder.ts b/src/vs/platform/terminal/common/terminalRecorder.ts index 79a828cc220..417527a976f 100644 --- a/src/vs/platform/terminal/common/terminalRecorder.ts +++ b/src/vs/platform/terminal/common/terminalRecorder.ts @@ -91,7 +91,8 @@ export class TerminalRecorder { // No command restoration is needed when relaunching terminals commands: { isWindowsPty: false, - commands: [] + commands: [], + promptInputModel: undefined, } }; } diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index cacc170e5cd..d335e2c27cd 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -577,7 +577,8 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati if (!this._terminal || !this.capabilities.has(TerminalCapability.CommandDetection)) { return { isWindowsPty: false, - commands: [] + commands: [], + promptInputModel: undefined, }; } const result = this._createOrGetCommandDetection(this._terminal).serialize(); diff --git a/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts b/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts index 8c74c72b9c9..ebd0331692e 100644 --- a/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts +++ b/src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts @@ -38,7 +38,7 @@ export class ElectronPtyHostStarter extends Disposable implements IPtyHostStarte ) { super(); - this._lifecycleMainService.onWillShutdown(() => this._onWillShutdown.fire()); + this._register(this._lifecycleMainService.onWillShutdown(() => this._onWillShutdown.fire())); // Listen for new windows to establish connection directly to pty host validatedIpcMain.on('vscode:createPtyHostMessageChannel', (e, nonce) => this._onWindowConnection(e, nonce)); this._register(toDisposable(() => { diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index f91b87baed4..402dcc7b723 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -108,14 +108,16 @@ export class PtyHostService extends Disposable implements IPtyHostService { this._register(toDisposable(() => this._disposePtyHost())); this._resolveVariablesRequestStore = this._register(new RequestStore(undefined, this._logService)); - this._resolveVariablesRequestStore.onCreateRequest(this._onPtyHostRequestResolveVariables.fire, this._onPtyHostRequestResolveVariables); + this._register(this._resolveVariablesRequestStore.onCreateRequest(this._onPtyHostRequestResolveVariables.fire, this._onPtyHostRequestResolveVariables)); // Start the pty host when a window requests a connection, if the starter has that capability. if (this._ptyHostStarter.onRequestConnection) { - Event.once(this._ptyHostStarter.onRequestConnection)(() => this._ensurePtyHost()); + this._register(Event.once(this._ptyHostStarter.onRequestConnection)(() => this._ensurePtyHost())); } - this._ptyHostStarter.onWillShutdown?.(() => this._wasQuitRequested = true); + if (this._ptyHostStarter.onWillShutdown) { + this._register(this._ptyHostStarter.onWillShutdown(() => this._wasQuitRequested = true)); + } } private get _ignoreProcessNames(): string[] { diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 813fbe5c432..7349ac1e9db 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -157,6 +157,7 @@ export function getShellIntegrationInjection( } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, ''); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; if (options.shellIntegration.suggestEnabled) { envMixin['VSCODE_SUGGEST'] = '1'; } @@ -174,6 +175,7 @@ export function getShellIntegrationInjection( } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { newArgs, envMixin }; } logService.warn(`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`, shellLaunchConfig.args); @@ -195,6 +197,7 @@ export function getShellIntegrationInjection( } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { newArgs, envMixin }; } case 'fish': { @@ -220,6 +223,7 @@ export function getShellIntegrationInjection( } newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, ''); + envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { newArgs, envMixin }; } case 'zsh': { diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index a799870b548..2679ea3683a 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { exec } from 'child_process'; import { timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -10,7 +11,6 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import * as path from 'vs/base/common/path'; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { Promises } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -211,9 +211,9 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } if (injection.filesToCopy) { for (const f of injection.filesToCopy) { - await Promises.mkdir(path.dirname(f.dest), { recursive: true }); + await fs.promises.mkdir(path.dirname(f.dest), { recursive: true }); try { - await Promises.copyFile(f.source, f.dest); + await fs.promises.copyFile(f.source, f.dest); } catch { // Swallow error, this should only happen when multiple users are on the same // machine. Since the shell integration scripts rarely change, plus the other user @@ -241,7 +241,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private async _validateCwd(): Promise { try { - const result = await Promises.stat(this._initialCwd); + const result = await fs.promises.stat(this._initialCwd); if (!result.isDirectory()) { return { message: localize('launchFail.cwdNotDirectory', "Starting directory (cwd) \"{0}\" is not a directory", this._initialCwd.toString()) }; } @@ -268,7 +268,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } try { - const result = await Promises.stat(executable); + const result = await fs.promises.stat(executable); if (!result.isFile() && !result.isSymbolicLink()) { return { message: localize('launchFail.executableIsNotFileOrSymlink', "Path to shell executable \"{0}\" is not a file or a symlink", slc.executable) }; } @@ -608,7 +608,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } this._logService.trace('node-pty.IPty#pid'); try { - return await Promises.readlink(`/proc/${this._ptyProcess.pid}/cwd`); + return await fs.promises.readlink(`/proc/${this._ptyProcess.pid}/cwd`); } catch (error) { return this._initialCwd; } diff --git a/src/vs/platform/terminal/node/terminalProfiles.ts b/src/vs/platform/terminal/node/terminalProfiles.ts index e289fbc34fc..97a85375c95 100644 --- a/src/vs/platform/terminal/node/terminalProfiles.ts +++ b/src/vs/platform/terminal/node/terminalProfiles.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as cp from 'child_process'; import { Codicon } from 'vs/base/common/codicons'; import { basename, delimiter, normalize } from 'vs/base/common/path'; @@ -38,7 +39,7 @@ export function detectAvailableProfiles( ): Promise { fsProvider = fsProvider || { existsFile: pfs.SymlinkSupport.existsFile, - readFile: pfs.Promises.readFile + readFile: fs.promises.readFile }; if (isWindows) { return detectAvailableWindowsProfiles( diff --git a/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts b/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts index 4566eb4b008..667442dd8c9 100644 --- a/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts +++ b/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts @@ -2,10 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable local/code-import-patterns */ -// eslint-disable-next-line local/code-import-patterns, local/code-amd-node-module -import { Terminal } from '@xterm/headless'; - +import type { Terminal } from '@xterm/xterm'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { PromptInputModel, type IPromptInputModelState } from 'vs/platform/terminal/common/capabilities/commandDetection/promptInputModel'; @@ -13,6 +12,7 @@ import { Emitter } from 'vs/base/common/event'; import type { ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; import { notDeepStrictEqual, strictEqual } from 'assert'; import { timeout } from 'vs/base/common/async'; +import { importAMDNodeModule } from 'vs/amdX'; suite('PromptInputModel', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -58,8 +58,9 @@ suite('PromptInputModel', () => { strictEqual(promptInputModel.cursorIndex, cursorIndex, `value=${promptInputModel.value}`); } - setup(() => { - xterm = store.add(new Terminal({ allowProposedApi: true })); + setup(async () => { + const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; + xterm = store.add(new TerminalCtor({ allowProposedApi: true })); onCommandStart = store.add(new Emitter()); onCommandExecuted = store.add(new Emitter()); promptInputModel = store.add(new PromptInputModel(xterm, onCommandStart.event, onCommandExecuted.event, new NullLogService)); @@ -469,6 +470,26 @@ suite('PromptInputModel', () => { }); }); + suite('wrapped line (non-continuation)', () => { + test('basic wrapped line', async () => { + xterm.resize(5, 10); + + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('ech'); + await assertPromptInput(`ech|`); + + await writePromise('o '); + await assertPromptInput(`echo |`); + + await writePromise('"a"'); + // HACK: Trailing whitespace is due to flaky detection in wrapped lines (but it doesn't matter much) + await assertPromptInput(`echo "a"| `); + }); + }); + // To "record a session" for these tests: // - Enable debug logging // - Open and clear Terminal output channel diff --git a/src/vs/platform/terminal/test/common/terminalRecorder.test.ts b/src/vs/platform/terminal/test/common/terminalRecorder.test.ts index b1c523a2228..66b317c468b 100644 --- a/src/vs/platform/terminal/test/common/terminalRecorder.test.ts +++ b/src/vs/platform/terminal/test/common/terminalRecorder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index 2f68a7be2df..dd4e198d7d4 100644 --- a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -186,7 +186,8 @@ suite('platform - terminalEnvironment', () => { `${repoRoot}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh` ], envMixin: { - VSCODE_INJECTION: '1' + VSCODE_INJECTION: '1', + VSCODE_STABLE: '0' } }); deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService), enabledExpectedResult); @@ -201,7 +202,8 @@ suite('platform - terminalEnvironment', () => { ], envMixin: { VSCODE_INJECTION: '1', - VSCODE_SHELL_LOGIN: '1' + VSCODE_SHELL_LOGIN: '1', + VSCODE_STABLE: '0' } }); test('when array', () => { diff --git a/src/vs/platform/theme/browser/defaultStyles.ts b/src/vs/platform/theme/browser/defaultStyles.ts index 871178faf38..a0df50920ab 100644 --- a/src/vs/platform/theme/browser/defaultStyles.ts +++ b/src/vs/platform/theme/browser/defaultStyles.ts @@ -176,7 +176,7 @@ export const defaultListStyles: IListStyles = { treeInactiveIndentGuidesStroke: asCssVariable(treeInactiveIndentGuidesStroke), treeStickyScrollBackground: undefined, treeStickyScrollBorder: undefined, - treeStickyScrollShadow: undefined, + treeStickyScrollShadow: asCssVariable(scrollbarShadow), tableColumnsBorder: asCssVariable(tableColumnsBorder), tableOddRowsBackgroundColor: asCssVariable(tableOddRowsBackgroundColor), }; diff --git a/src/vs/platform/theme/common/colorUtils.ts b/src/vs/platform/theme/common/colorUtils.ts index 2388e7cb702..14ceea8847b 100644 --- a/src/vs/platform/theme/common/colorUtils.ts +++ b/src/vs/platform/theme/common/colorUtils.ts @@ -7,10 +7,11 @@ import { assertNever } from 'vs/base/common/assert'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; -import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; +import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import * as platform from 'vs/platform/registry/common/platform'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; +import * as nls from 'vs/nls'; // ------ API types @@ -19,7 +20,7 @@ export type ColorIdentifier = string; export interface ColorContribution { readonly id: ColorIdentifier; readonly description: string; - readonly defaults: ColorDefaults | null; + readonly defaults: ColorDefaults | ColorValue | null; readonly needsTransparency: boolean; readonly deprecationMessage: string | undefined; } @@ -68,6 +69,9 @@ export interface ColorDefaults { hcLight: ColorValue | null; } +export function isColorDefaults(value: unknown): value is ColorDefaults { + return value !== null && typeof value === 'object' && 'light' in value && 'dark' in value; +} /** * A Color Value is either a color literal, a reference to an other color or a derived color @@ -79,6 +83,8 @@ export const Extensions = { ColorContribution: 'base.contributions.colors' }; +export const DEFAULT_COLOR_CONFIG_VALUE = 'default'; + export interface IColorRegistry { readonly onDidChangeSchema: Event; @@ -117,33 +123,56 @@ export interface IColorRegistry { */ getColorReferenceSchema(): IJSONSchema; + /** + * Notify when the color theme or settings change. + */ + notifyThemeUpdate(theme: IColorTheme): void; + } +type IJSONSchemaForColors = IJSONSchema & { properties: { [name: string]: { oneOf: [IJSONSchemaWithSnippets, IJSONSchema] } } }; +type IJSONSchemaWithSnippets = IJSONSchema & { defaultSnippets: IJSONSchemaSnippet[] }; + class ColorRegistry implements IColorRegistry { private readonly _onDidChangeSchema = new Emitter(); readonly onDidChangeSchema: Event = this._onDidChangeSchema.event; private colorsById: { [key: string]: ColorContribution }; - private colorSchema: IJSONSchema & { properties: IJSONSchemaMap } = { type: 'object', properties: {} }; + private colorSchema: IJSONSchemaForColors = { type: 'object', properties: {} }; private colorReferenceSchema: IJSONSchema & { enum: string[]; enumDescriptions: string[] } = { type: 'string', enum: [], enumDescriptions: [] }; constructor() { this.colorsById = {}; } - public registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { + public notifyThemeUpdate(colorThemeData: IColorTheme) { + for (const key of Object.keys(this.colorsById)) { + const color = colorThemeData.getColor(key); + if (color) { + this.colorSchema.properties[key].oneOf[0].defaultSnippets[0].body = `\${1:${color.toString()}}`; + } + } + this._onDidChangeSchema.fire(); + } + + public registerColor(id: string, defaults: ColorDefaults | ColorValue | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { const colorContribution: ColorContribution = { id, description, defaults, needsTransparency, deprecationMessage }; this.colorsById[id] = colorContribution; - const propertySchema: IJSONSchema = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; + const propertySchema: IJSONSchemaWithSnippets = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; if (deprecationMessage) { propertySchema.deprecationMessage = deprecationMessage; } if (needsTransparency) { propertySchema.pattern = '^#(?:(?[0-9a-fA-f]{3}[0-9a-eA-E])|(?:[0-9a-fA-F]{6}(?:(?![fF]{2})(?:[0-9a-fA-F]{2}))))?$'; - propertySchema.patternErrorMessage = 'This color must be transparent or it will obscure content'; + propertySchema.patternErrorMessage = nls.localize('transparecyRequired', 'This color must be transparent or it will obscure content'); } - this.colorSchema.properties[id] = propertySchema; + this.colorSchema.properties[id] = { + oneOf: [ + propertySchema, + { type: 'string', const: DEFAULT_COLOR_CONFIG_VALUE, description: nls.localize('useDefault', 'Use the default color.') } + ] + }; this.colorReferenceSchema.enum.push(id); this.colorReferenceSchema.enumDescriptions.push(description); @@ -169,8 +198,8 @@ class ColorRegistry implements IColorRegistry { public resolveDefaultColor(id: ColorIdentifier, theme: IColorTheme): Color | undefined { const colorDesc = this.colorsById[id]; - if (colorDesc && colorDesc.defaults) { - const colorValue = colorDesc.defaults[theme.type]; + if (colorDesc?.defaults) { + const colorValue = isColorDefaults(colorDesc.defaults) ? colorDesc.defaults[theme.type] : colorDesc.defaults; return resolveColorValue(colorValue, theme); } return undefined; @@ -203,7 +232,7 @@ const colorRegistry = new ColorRegistry(); platform.Registry.add(Extensions.ColorContribution, colorRegistry); -export function registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency?: boolean, deprecationMessage?: string): ColorIdentifier { +export function registerColor(id: string, defaults: ColorDefaults | ColorValue | null, description: string, needsTransparency?: boolean, deprecationMessage?: string): ColorIdentifier { return colorRegistry.registerColor(id, defaults, description, needsTransparency, deprecationMessage); } @@ -319,6 +348,7 @@ const schemaRegistry = platform.Registry.as(JSONExten schemaRegistry.registerSchema(workbenchColorsSchemaId, colorRegistry.getColorSchema()); const delayer = new RunOnceScheduler(() => schemaRegistry.notifySchemaChanged(workbenchColorsSchemaId), 200); + colorRegistry.onDidChangeSchema(() => { if (!delayer.isScheduled()) { delayer.schedule(); diff --git a/src/vs/platform/theme/common/colors/baseColors.ts b/src/vs/platform/theme/common/colors/baseColors.ts index 1d19b3adc1f..baf6b86f27f 100644 --- a/src/vs/platform/theme/common/colors/baseColors.ts +++ b/src/vs/platform/theme/common/colors/baseColors.ts @@ -43,7 +43,7 @@ export const activeContrastBorder = registerColor('contrastActiveBorder', nls.localize('activeContrastBorder', "An extra border around active elements to separate them from others for greater contrast.")); export const selectionBackground = registerColor('selection.background', - { light: null, dark: null, hcDark: null, hcLight: null }, + null, nls.localize('selectionBackground', "The background color of text selections in the workbench (e.g. for input fields or text areas). Note that this does not apply to selections within the editor.")); diff --git a/src/vs/platform/theme/common/colors/chartsColors.ts b/src/vs/platform/theme/common/colors/chartsColors.ts index eb63b602234..a35e296d2ad 100644 --- a/src/vs/platform/theme/common/colors/chartsColors.ts +++ b/src/vs/platform/theme/common/colors/chartsColors.ts @@ -12,27 +12,27 @@ import { minimapFindMatch } from 'vs/platform/theme/common/colors/minimapColors' export const chartsForeground = registerColor('charts.foreground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + foreground, nls.localize('chartsForeground', "The foreground color used in charts.")); export const chartsLines = registerColor('charts.lines', - { dark: transparent(foreground, .5), light: transparent(foreground, .5), hcDark: transparent(foreground, .5), hcLight: transparent(foreground, .5) }, + transparent(foreground, .5), nls.localize('chartsLines', "The color used for horizontal lines in charts.")); export const chartsRed = registerColor('charts.red', - { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, + editorErrorForeground, nls.localize('chartsRed', "The red color used in chart visualizations.")); export const chartsBlue = registerColor('charts.blue', - { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, + editorInfoForeground, nls.localize('chartsBlue', "The blue color used in chart visualizations.")); export const chartsYellow = registerColor('charts.yellow', - { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, + editorWarningForeground, nls.localize('chartsYellow', "The yellow color used in chart visualizations.")); export const chartsOrange = registerColor('charts.orange', - { dark: minimapFindMatch, light: minimapFindMatch, hcDark: minimapFindMatch, hcLight: minimapFindMatch }, + minimapFindMatch, nls.localize('chartsOrange', "The orange color used in chart visualizations.")); export const chartsGreen = registerColor('charts.green', diff --git a/src/vs/platform/theme/common/colors/editorColors.ts b/src/vs/platform/theme/common/colors/editorColors.ts index 4116f5ec141..cac6dea162c 100644 --- a/src/vs/platform/theme/common/colors/editorColors.ts +++ b/src/vs/platform/theme/common/colors/editorColors.ts @@ -26,7 +26,7 @@ export const editorForeground = registerColor('editor.foreground', export const editorStickyScrollBackground = registerColor('editorStickyScroll.background', - { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, + editorBackground, nls.localize('editorStickyScrollBackground', "Background color of sticky scroll in the editor")); export const editorStickyScrollHoverBackground = registerColor('editorStickyScrollHover.background', @@ -38,7 +38,7 @@ export const editorStickyScrollBorder = registerColor('editorStickyScroll.border nls.localize('editorStickyScrollBorder', "Border color of sticky scroll in the editor")); export const editorStickyScrollShadow = registerColor('editorStickyScroll.shadow', - { dark: scrollbarShadow, light: scrollbarShadow, hcDark: scrollbarShadow, hcLight: scrollbarShadow }, + scrollbarShadow, nls.localize('editorStickyScrollShadow', " Shadow color of sticky scroll in the editor")); @@ -47,7 +47,7 @@ export const editorWidgetBackground = registerColor('editorWidget.background', nls.localize('editorWidgetBackground', 'Background color of editor widgets, such as find/replace.')); export const editorWidgetForeground = registerColor('editorWidget.foreground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + foreground, nls.localize('editorWidgetForeground', 'Foreground color of editor widgets, such as find/replace.')); export const editorWidgetBorder = registerColor('editorWidget.border', @@ -55,12 +55,12 @@ export const editorWidgetBorder = registerColor('editorWidget.border', nls.localize('editorWidgetBorder', 'Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.')); export const editorWidgetResizeBorder = registerColor('editorWidget.resizeBorder', - { light: null, dark: null, hcDark: null, hcLight: null }, + null, nls.localize('editorWidgetResizeBorder', "Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget.")); export const editorErrorBackground = registerColor('editorError.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('editorError.background', 'Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorErrorForeground = registerColor('editorError.foreground', @@ -73,7 +73,7 @@ export const editorErrorBorder = registerColor('editorError.border', export const editorWarningBackground = registerColor('editorWarning.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('editorWarning.background', 'Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorWarningForeground = registerColor('editorWarning.foreground', @@ -86,7 +86,7 @@ export const editorWarningBorder = registerColor('editorWarning.border', export const editorInfoBackground = registerColor('editorInfo.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('editorInfo.background', 'Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorInfoForeground = registerColor('editorInfo.foreground', @@ -141,10 +141,18 @@ export const editorFindMatch = registerColor('editor.findMatchBackground', { light: '#A8AC94', dark: '#515C6A', hcDark: null, hcLight: null }, nls.localize('editorFindMatch', "Color of the current search match.")); +export const editorFindMatchForeground = registerColor('editor.findMatchForeground', + null, + nls.localize('editorFindMatchForeground', "Text color of the current search match.")); + export const editorFindMatchHighlight = registerColor('editor.findMatchHighlightBackground', { light: '#EA5C0055', dark: '#EA5C0055', hcDark: null, hcLight: null }, nls.localize('findMatchHighlight', "Color of the other search matches. The color must not be opaque so as not to hide underlying decorations."), true); +export const editorFindMatchHighlightForeground = registerColor('editor.findMatchHighlightForeground', + null, + nls.localize('findMatchHighlightForeground', "Foreground color of the other search matches."), true); + export const editorFindRangeHighlight = registerColor('editor.findRangeHighlightBackground', { dark: '#3a3d4166', light: '#b4b4b44d', hcDark: null, hcLight: null }, nls.localize('findRangeHighlight', "Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations."), true); @@ -169,15 +177,15 @@ export const editorHoverHighlight = registerColor('editor.hoverHighlightBackgrou nls.localize('hoverHighlight', 'Highlight below the word for which a hover is shown. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorHoverBackground = registerColor('editorHoverWidget.background', - { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('hoverBackground', 'Background color of the editor hover.')); export const editorHoverForeground = registerColor('editorHoverWidget.foreground', - { light: editorWidgetForeground, dark: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, + editorWidgetForeground, nls.localize('hoverForeground', 'Foreground color of the editor hover.')); export const editorHoverBorder = registerColor('editorHoverWidget.border', - { light: editorWidgetBorder, dark: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, + editorWidgetBorder, nls.localize('hoverBorder', 'Border color of the editor hover.')); export const editorHoverStatusBarBackground = registerColor('editorHoverWidget.statusBarBackground', @@ -196,19 +204,19 @@ export const editorInlayHintBackground = registerColor('editorInlayHint.backgrou nls.localize('editorInlayHintBackground', 'Background color of inline hints')); export const editorInlayHintTypeForeground = registerColor('editorInlayHint.typeForeground', - { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, + editorInlayHintForeground, nls.localize('editorInlayHintForegroundTypes', 'Foreground color of inline hints for types')); export const editorInlayHintTypeBackground = registerColor('editorInlayHint.typeBackground', - { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, + editorInlayHintBackground, nls.localize('editorInlayHintBackgroundTypes', 'Background color of inline hints for types')); export const editorInlayHintParameterForeground = registerColor('editorInlayHint.parameterForeground', - { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, + editorInlayHintForeground, nls.localize('editorInlayHintForegroundParameter', 'Foreground color of inline hints for parameters')); export const editorInlayHintParameterBackground = registerColor('editorInlayHint.parameterBackground', - { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, + editorInlayHintBackground, nls.localize('editorInlayHintBackgroundParameter', 'Background color of inline hints for parameters')); @@ -223,7 +231,7 @@ export const editorLightBulbAutoFixForeground = registerColor('editorLightBulbAu nls.localize('editorLightBulbAutoFixForeground', "The color used for the lightbulb auto fix actions icon.")); export const editorLightBulbAiForeground = registerColor('editorLightBulbAi.foreground', - { dark: editorLightBulbForeground, light: editorLightBulbForeground, hcDark: editorLightBulbForeground, hcLight: editorLightBulbForeground }, + editorLightBulbForeground, nls.localize('editorLightBulbAiForeground', "The color used for the lightbulb AI icon.")); @@ -234,11 +242,11 @@ export const snippetTabstopHighlightBackground = registerColor('editor.snippetTa nls.localize('snippetTabstopHighlightBackground', "Highlight background color of a snippet tabstop.")); export const snippetTabstopHighlightBorder = registerColor('editor.snippetTabstopHighlightBorder', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('snippetTabstopHighlightBorder', "Highlight border color of a snippet tabstop.")); export const snippetFinalTabstopHighlightBackground = registerColor('editor.snippetFinalTabstopHighlightBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('snippetFinalTabstopHighlightBackground', "Highlight background color of the final tabstop of a snippet.")); export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetFinalTabstopHighlightBorder', @@ -270,20 +278,20 @@ export const diffRemovedLine = registerColor('diffEditor.removedLineBackground', export const diffInsertedLineGutter = registerColor('diffEditorGutter.insertedLineBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorInsertedLineGutter', 'Background color for the margin where lines got inserted.')); export const diffRemovedLineGutter = registerColor('diffEditorGutter.removedLineBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorRemovedLineGutter', 'Background color for the margin where lines got removed.')); export const diffOverviewRulerInserted = registerColor('diffEditorOverview.insertedForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorOverviewInserted', 'Diff overview ruler foreground for inserted content.')); export const diffOverviewRulerRemoved = registerColor('diffEditorOverview.removedForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('diffEditorOverviewRemoved', 'Diff overview ruler foreground for removed content.')); @@ -306,11 +314,11 @@ export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', export const diffUnchangedRegionBackground = registerColor('diffEditor.unchangedRegionBackground', - { dark: 'sideBar.background', light: 'sideBar.background', hcDark: 'sideBar.background', hcLight: 'sideBar.background' }, + 'sideBar.background', nls.localize('diffEditor.unchangedRegionBackground', "The background color of unchanged blocks in the diff editor.")); export const diffUnchangedRegionForeground = registerColor('diffEditor.unchangedRegionForeground', - { dark: 'foreground', light: 'foreground', hcDark: 'foreground', hcLight: 'foreground' }, + 'foreground', nls.localize('diffEditor.unchangedRegionForeground', "The foreground color of unchanged blocks in the diff editor.")); export const diffUnchangedTextBackground = registerColor('diffEditor.unchangedCodeBackground', @@ -347,11 +355,11 @@ export const toolbarActiveBackground = registerColor('toolbar.activeBackground', // ----- breadcumbs export const breadcrumbsForeground = registerColor('breadcrumb.foreground', - { light: transparent(foreground, 0.8), dark: transparent(foreground, 0.8), hcDark: transparent(foreground, 0.8), hcLight: transparent(foreground, 0.8) }, + transparent(foreground, 0.8), nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); export const breadcrumbsBackground = registerColor('breadcrumb.background', - { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, + editorBackground, nls.localize('breadcrumbsBackground', "Background color of breadcrumb items.")); export const breadcrumbsFocusForeground = registerColor('breadcrumb.focusForeground', @@ -363,7 +371,7 @@ export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.ac nls.localize('breadcrumbsSelectedForeground', "Color of selected breadcrumb items.")); export const breadcrumbsPickerBackground = registerColor('breadcrumbPicker.background', - { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('breadcrumbsSelectedBackground', "Background color of breadcrumb item picker.")); @@ -381,7 +389,7 @@ export const mergeCurrentHeaderBackground = registerColor('merge.currentHeaderBa nls.localize('mergeCurrentHeaderBackground', 'Current header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeCurrentContentBackground = registerColor('merge.currentContentBackground', - { dark: transparent(mergeCurrentHeaderBackground, contentTransparency), light: transparent(mergeCurrentHeaderBackground, contentTransparency), hcDark: transparent(mergeCurrentHeaderBackground, contentTransparency), hcLight: transparent(mergeCurrentHeaderBackground, contentTransparency) }, + transparent(mergeCurrentHeaderBackground, contentTransparency), nls.localize('mergeCurrentContentBackground', 'Current content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeIncomingHeaderBackground = registerColor('merge.incomingHeaderBackground', @@ -389,7 +397,7 @@ export const mergeIncomingHeaderBackground = registerColor('merge.incomingHeader nls.localize('mergeIncomingHeaderBackground', 'Incoming header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeIncomingContentBackground = registerColor('merge.incomingContentBackground', - { dark: transparent(mergeIncomingHeaderBackground, contentTransparency), light: transparent(mergeIncomingHeaderBackground, contentTransparency), hcDark: transparent(mergeIncomingHeaderBackground, contentTransparency), hcLight: transparent(mergeIncomingHeaderBackground, contentTransparency) }, + transparent(mergeIncomingHeaderBackground, contentTransparency), nls.localize('mergeIncomingContentBackground', 'Incoming content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeCommonHeaderBackground = registerColor('merge.commonHeaderBackground', @@ -397,7 +405,7 @@ export const mergeCommonHeaderBackground = registerColor('merge.commonHeaderBack nls.localize('mergeCommonHeaderBackground', 'Common ancestor header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeCommonContentBackground = registerColor('merge.commonContentBackground', - { dark: transparent(mergeCommonHeaderBackground, contentTransparency), light: transparent(mergeCommonHeaderBackground, contentTransparency), hcDark: transparent(mergeCommonHeaderBackground, contentTransparency), hcLight: transparent(mergeCommonHeaderBackground, contentTransparency) }, + transparent(mergeCommonHeaderBackground, contentTransparency), nls.localize('mergeCommonContentBackground', 'Common ancestor content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); export const mergeBorder = registerColor('merge.border', @@ -422,20 +430,20 @@ export const overviewRulerFindMatchForeground = registerColor('editorOverviewRul nls.localize('overviewRulerFindMatchForeground', 'Overview ruler marker color for find matches. The color must not be opaque so as not to hide underlying decorations.'), true); export const overviewRulerSelectionHighlightForeground = registerColor('editorOverviewRuler.selectionHighlightForeground', - { dark: '#A0A0A0CC', light: '#A0A0A0CC', hcDark: '#A0A0A0CC', hcLight: '#A0A0A0CC' }, + '#A0A0A0CC', nls.localize('overviewRulerSelectionHighlightForeground', 'Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.'), true); // ----- problems export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', - { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, + editorErrorForeground, nls.localize('problemsErrorIconForeground', "The color used for the problems error icon.")); export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', - { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, + editorWarningForeground, nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon.")); export const problemsInfoIconForeground = registerColor('problemsInfoIcon.foreground', - { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, + editorInfoForeground, nls.localize('problemsInfoIconForeground', "The color used for the problems info icon.")); diff --git a/src/vs/platform/theme/common/colors/inputColors.ts b/src/vs/platform/theme/common/colors/inputColors.ts index dc38222d402..c79c1d2840b 100644 --- a/src/vs/platform/theme/common/colors/inputColors.ts +++ b/src/vs/platform/theme/common/colors/inputColors.ts @@ -21,7 +21,7 @@ export const inputBackground = registerColor('input.background', nls.localize('inputBoxBackground', "Input box background.")); export const inputForeground = registerColor('input.foreground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, + foreground, nls.localize('inputBoxForeground', "Input box foreground.")); export const inputBorder = registerColor('input.border', @@ -110,11 +110,11 @@ export const selectBorder = registerColor('dropdown.border', // ------ button export const buttonForeground = registerColor('button.foreground', - { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: Color.white }, + Color.white, nls.localize('buttonForeground', "Button foreground color.")); export const buttonSeparator = registerColor('button.separator', - { dark: transparent(buttonForeground, .4), light: transparent(buttonForeground, .4), hcDark: transparent(buttonForeground, .4), hcLight: transparent(buttonForeground, .4) }, + transparent(buttonForeground, .4), nls.localize('buttonSeparator', "Button separator color.")); export const buttonBackground = registerColor('button.background', @@ -126,7 +126,7 @@ export const buttonHoverBackground = registerColor('button.hoverBackground', nls.localize('buttonHoverBackground', "Button background color when hovering.")); export const buttonBorder = registerColor('button.border', - { dark: contrastBorder, light: contrastBorder, hcDark: contrastBorder, hcLight: contrastBorder }, + contrastBorder, nls.localize('buttonBorder', "Button border color.")); export const buttonSecondaryForeground = registerColor('button.secondaryForeground', @@ -145,23 +145,23 @@ export const buttonSecondaryHoverBackground = registerColor('button.secondaryHov // ------ checkbox export const checkboxBackground = registerColor('checkbox.background', - { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, + selectBackground, nls.localize('checkbox.background', "Background color of checkbox widget.")); export const checkboxSelectBackground = registerColor('checkbox.selectBackground', - { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('checkbox.select.background', "Background color of checkbox widget when the element it's in is selected.")); export const checkboxForeground = registerColor('checkbox.foreground', - { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, + selectForeground, nls.localize('checkbox.foreground', "Foreground color of checkbox widget.")); export const checkboxBorder = registerColor('checkbox.border', - { dark: selectBorder, light: selectBorder, hcDark: selectBorder, hcLight: selectBorder }, + selectBorder, nls.localize('checkbox.border', "Border color of checkbox widget.")); export const checkboxSelectBorder = registerColor('checkbox.selectBorder', - { dark: iconForeground, light: iconForeground, hcDark: iconForeground, hcLight: iconForeground }, + iconForeground, nls.localize('checkbox.select.border', "Border color of checkbox widget when the element it's in is selected.")); diff --git a/src/vs/platform/theme/common/colors/listColors.ts b/src/vs/platform/theme/common/colors/listColors.ts index b6f51e3696b..dd5c405199c 100644 --- a/src/vs/platform/theme/common/colors/listColors.ts +++ b/src/vs/platform/theme/common/colors/listColors.ts @@ -15,11 +15,11 @@ import { editorWidgetBackground, editorFindMatchHighlightBorder, editorFindMatch export const listFocusBackground = registerColor('list.focusBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusForeground = registerColor('list.focusForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusOutline = registerColor('list.focusOutline', @@ -27,7 +27,7 @@ export const listFocusOutline = registerColor('list.focusOutline', nls.localize('listFocusOutline', "List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusAndSelectionOutline = registerColor('list.focusAndSelectionOutline', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listFocusAndSelectionOutline', "List/Tree outline color for the focused item when the list/tree is active and selected. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', @@ -39,7 +39,7 @@ export const listActiveSelectionForeground = registerColor('list.activeSelection nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionIconForeground = registerColor('list.activeSelectionIconForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listActiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', @@ -47,19 +47,19 @@ export const listInactiveSelectionBackground = registerColor('list.inactiveSelec nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionIconForeground = registerColor('list.inactiveSelectionIconForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveFocusOutline = registerColor('list.inactiveFocusOutline', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listInactiveFocusOutline', "List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listHoverBackground = registerColor('list.hoverBackground', @@ -67,7 +67,7 @@ export const listHoverBackground = registerColor('list.hoverBackground', nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); export const listHoverForeground = registerColor('list.hoverForeground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); export const listDropOverBackground = registerColor('list.dropBackground', @@ -109,7 +109,7 @@ export const listFilterWidgetNoMatchesOutline = registerColor('listFilterWidget. nls.localize('listFilterWidgetNoMatchesOutline', 'Outline color of the type filter widget in lists and trees, when there are no matches.')); export const listFilterWidgetShadow = registerColor('listFilterWidget.shadow', - { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, + widgetShadow, nls.localize('listFilterWidgetShadow', 'Shadow color of the type filter widget in lists and trees.')); export const listFilterMatchHighlight = registerColor('list.filterMatchBackground', @@ -132,7 +132,7 @@ export const treeIndentGuidesStroke = registerColor('tree.indentGuidesStroke', nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); export const treeInactiveIndentGuidesStroke = registerColor('tree.inactiveIndentGuidesStroke', - { dark: transparent(treeIndentGuidesStroke, 0.4), light: transparent(treeIndentGuidesStroke, 0.4), hcDark: transparent(treeIndentGuidesStroke, 0.4), hcLight: transparent(treeIndentGuidesStroke, 0.4) }, + transparent(treeIndentGuidesStroke, 0.4), nls.localize('treeInactiveIndentGuidesStroke', "Tree stroke color for the indentation guides that are not active.")); diff --git a/src/vs/platform/theme/common/colors/menuColors.ts b/src/vs/platform/theme/common/colors/menuColors.ts index 6fa9a0ec326..05bf5491952 100644 --- a/src/vs/platform/theme/common/colors/menuColors.ts +++ b/src/vs/platform/theme/common/colors/menuColors.ts @@ -19,19 +19,19 @@ export const menuBorder = registerColor('menu.border', nls.localize('menuBorder', "Border color of menus.")); export const menuForeground = registerColor('menu.foreground', - { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, + selectForeground, nls.localize('menuForeground', "Foreground color of menu items.")); export const menuBackground = registerColor('menu.background', - { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, + selectBackground, nls.localize('menuBackground', "Background color of menu items.")); export const menuSelectionForeground = registerColor('menu.selectionForeground', - { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, + listActiveSelectionForeground, nls.localize('menuSelectionForeground', "Foreground color of the selected menu item in menus.")); export const menuSelectionBackground = registerColor('menu.selectionBackground', - { dark: listActiveSelectionBackground, light: listActiveSelectionBackground, hcDark: listActiveSelectionBackground, hcLight: listActiveSelectionBackground }, + listActiveSelectionBackground, nls.localize('menuSelectionBackground', "Background color of the selected menu item in menus.")); export const menuSelectionBorder = registerColor('menu.selectionBorder', diff --git a/src/vs/platform/theme/common/colors/minimapColors.ts b/src/vs/platform/theme/common/colors/minimapColors.ts index 0b051994d09..ade38578c28 100644 --- a/src/vs/platform/theme/common/colors/minimapColors.ts +++ b/src/vs/platform/theme/common/colors/minimapColors.ts @@ -39,21 +39,21 @@ export const minimapError = registerColor('minimap.errorHighlight', nls.localize('minimapError', 'Minimap marker color for errors.')); export const minimapBackground = registerColor('minimap.background', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, nls.localize('minimapBackground', "Minimap background color.")); export const minimapForegroundOpacity = registerColor('minimap.foregroundOpacity', - { dark: Color.fromHex('#000f'), light: Color.fromHex('#000f'), hcDark: Color.fromHex('#000f'), hcLight: Color.fromHex('#000f') }, + Color.fromHex('#000f'), nls.localize('minimapForegroundOpacity', 'Opacity of foreground elements rendered in the minimap. For example, "#000000c0" will render the elements with 75% opacity.')); export const minimapSliderBackground = registerColor('minimapSlider.background', - { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hcDark: transparent(scrollbarSliderBackground, 0.5), hcLight: transparent(scrollbarSliderBackground, 0.5) }, + transparent(scrollbarSliderBackground, 0.5), nls.localize('minimapSliderBackground', "Minimap slider background color.")); export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', - { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hcDark: transparent(scrollbarSliderHoverBackground, 0.5), hcLight: transparent(scrollbarSliderHoverBackground, 0.5) }, + transparent(scrollbarSliderHoverBackground, 0.5), nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering.")); export const minimapSliderActiveBackground = registerColor('minimapSlider.activeBackground', - { light: transparent(scrollbarSliderActiveBackground, 0.5), dark: transparent(scrollbarSliderActiveBackground, 0.5), hcDark: transparent(scrollbarSliderActiveBackground, 0.5), hcLight: transparent(scrollbarSliderActiveBackground, 0.5) }, + transparent(scrollbarSliderActiveBackground, 0.5), nls.localize('minimapSliderActiveBackground', "Minimap slider background color when clicked on.")); diff --git a/src/vs/platform/theme/common/colors/miscColors.ts b/src/vs/platform/theme/common/colors/miscColors.ts index 5a2ea49b702..42a00e23e6a 100644 --- a/src/vs/platform/theme/common/colors/miscColors.ts +++ b/src/vs/platform/theme/common/colors/miscColors.ts @@ -16,7 +16,7 @@ import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colors/bas // ----- sash export const sashHoverBorder = registerColor('sash.hoverBorder', - { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, + focusBorder, nls.localize('sashActiveBorder', "Border color of active sashes.")); diff --git a/src/vs/platform/theme/common/colors/quickpickColors.ts b/src/vs/platform/theme/common/colors/quickpickColors.ts index 7f8fc271a6e..3b109a21872 100644 --- a/src/vs/platform/theme/common/colors/quickpickColors.ts +++ b/src/vs/platform/theme/common/colors/quickpickColors.ts @@ -15,11 +15,11 @@ import { listActiveSelectionBackground, listActiveSelectionForeground, listActiv export const quickInputBackground = registerColor('quickInput.background', - { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, + editorWidgetBackground, nls.localize('pickerBackground', "Quick picker background color. The quick picker widget is the container for pickers like the command palette.")); export const quickInputForeground = registerColor('quickInput.foreground', - { dark: editorWidgetForeground, light: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, + editorWidgetForeground, nls.localize('pickerForeground', "Quick picker foreground color. The quick picker widget is the container for pickers like the command palette.")); export const quickInputTitleBackground = registerColor('quickInputTitle.background', @@ -35,15 +35,15 @@ export const pickerGroupBorder = registerColor('pickerGroup.border', nls.localize('pickerGroupBorder', "Quick picker color for grouping borders.")); export const _deprecatedQuickInputListFocusBackground = registerColor('quickInput.list.focusBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, '', undefined, + null, '', undefined, nls.localize('quickInput.list.focusBackground deprecation', "Please use quickInputList.focusBackground instead")); export const quickInputListFocusForeground = registerColor('quickInputList.focusForeground', - { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, + listActiveSelectionForeground, nls.localize('quickInput.listFocusForeground', "Quick picker foreground color for the focused item.")); export const quickInputListFocusIconForeground = registerColor('quickInputList.focusIconForeground', - { dark: listActiveSelectionIconForeground, light: listActiveSelectionIconForeground, hcDark: listActiveSelectionIconForeground, hcLight: listActiveSelectionIconForeground }, + listActiveSelectionIconForeground, nls.localize('quickInput.listFocusIconForeground', "Quick picker icon foreground color for the focused item.")); export const quickInputListFocusBackground = registerColor('quickInputList.focusBackground', diff --git a/src/vs/platform/theme/electron-main/themeMainService.ts b/src/vs/platform/theme/electron-main/themeMainService.ts index caef715f2da..e332feed264 100644 --- a/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/src/vs/platform/theme/electron-main/themeMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, nativeTheme } from 'electron'; +import electron from 'electron'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; @@ -64,30 +64,30 @@ export class ThemeMainService extends Disposable implements IThemeMainService { this.updateSystemColorTheme(); // Color Scheme changes - this._register(Event.fromNodeEventEmitter(nativeTheme, 'updated')(() => this._onDidChangeColorScheme.fire(this.getColorScheme()))); + this._register(Event.fromNodeEventEmitter(electron.nativeTheme, 'updated')(() => this._onDidChangeColorScheme.fire(this.getColorScheme()))); } private updateSystemColorTheme(): void { if (isLinux || this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) { // only with `system` we can detect the system color scheme - nativeTheme.themeSource = 'system'; + electron.nativeTheme.themeSource = 'system'; } else { switch (this.configurationService.getValue<'default' | 'auto' | 'light' | 'dark'>(ThemeSettings.SYSTEM_COLOR_THEME)) { case 'dark': - nativeTheme.themeSource = 'dark'; + electron.nativeTheme.themeSource = 'dark'; break; case 'light': - nativeTheme.themeSource = 'light'; + electron.nativeTheme.themeSource = 'light'; break; case 'auto': switch (this.getBaseTheme()) { - case 'vs': nativeTheme.themeSource = 'light'; break; - case 'vs-dark': nativeTheme.themeSource = 'dark'; break; - default: nativeTheme.themeSource = 'system'; + case 'vs': electron.nativeTheme.themeSource = 'light'; break; + case 'vs-dark': electron.nativeTheme.themeSource = 'dark'; break; + default: electron.nativeTheme.themeSource = 'system'; } break; default: - nativeTheme.themeSource = 'system'; + electron.nativeTheme.themeSource = 'system'; break; } @@ -97,23 +97,23 @@ export class ThemeMainService extends Disposable implements IThemeMainService { getColorScheme(): IColorScheme { if (isWindows) { // high contrast is refelected by the shouldUseInvertedColorScheme property - if (nativeTheme.shouldUseHighContrastColors) { + if (electron.nativeTheme.shouldUseHighContrastColors) { // shouldUseInvertedColorScheme is dark, !shouldUseInvertedColorScheme is light - return { dark: nativeTheme.shouldUseInvertedColorScheme, highContrast: true }; + return { dark: electron.nativeTheme.shouldUseInvertedColorScheme, highContrast: true }; } } else if (isMacintosh) { // high contrast is set if one of shouldUseInvertedColorScheme or shouldUseHighContrastColors is set, reflecting the 'Invert colours' and `Increase contrast` settings in MacOS - if (nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors) { - return { dark: nativeTheme.shouldUseDarkColors, highContrast: true }; + if (electron.nativeTheme.shouldUseInvertedColorScheme || electron.nativeTheme.shouldUseHighContrastColors) { + return { dark: electron.nativeTheme.shouldUseDarkColors, highContrast: true }; } } else if (isLinux) { // ubuntu gnome seems to have 3 states, light dark and high contrast - if (nativeTheme.shouldUseHighContrastColors) { + if (electron.nativeTheme.shouldUseHighContrastColors) { return { dark: true, highContrast: true }; } } return { - dark: nativeTheme.shouldUseDarkColors, + dark: electron.nativeTheme.shouldUseDarkColors, highContrast: false }; } @@ -170,7 +170,7 @@ export class ThemeMainService extends Disposable implements IThemeMainService { } private updateBackgroundColor(windowId: number, splash: IPartsSplash): void { - for (const window of BrowserWindow.getAllWindows()) { + for (const window of electron.BrowserWindow.getAllWindows()) { if (window.id === windowId) { window.setBackgroundColor(splash.colorInfo.background); break; diff --git a/src/vs/platform/tunnel/common/tunnel.ts b/src/vs/platform/tunnel/common/tunnel.ts index 86b4da4b409..b1433f2e745 100644 --- a/src/vs/platform/tunnel/common/tunnel.ts +++ b/src/vs/platform/tunnel/common/tunnel.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -215,7 +215,7 @@ export class DisposableTunnel { } } -export abstract class AbstractTunnelService implements ITunnelService { +export abstract class AbstractTunnelService extends Disposable implements ITunnelService { declare readonly _serviceBrand: undefined; private _onTunnelOpened: Emitter = new Emitter(); @@ -234,7 +234,7 @@ export abstract class AbstractTunnelService implements ITunnelService { public constructor( @ILogService protected readonly logService: ILogService, @IConfigurationService protected readonly configurationService: IConfigurationService - ) { } + ) { super(); } get hasTunnelProvider(): boolean { return !!this._tunnelProvider; @@ -308,7 +308,8 @@ export abstract class AbstractTunnelService implements ITunnelService { return tunnels; } - async dispose(): Promise { + override async dispose(): Promise { + super.dispose(); for (const portMap of this._tunnels.values()) { for (const { value } of portMap.values()) { await value.then(tunnel => typeof tunnel !== 'string' ? tunnel?.dispose() : undefined); diff --git a/src/vs/platform/tunnel/test/common/tunnel.test.ts b/src/vs/platform/tunnel/test/common/tunnel.test.ts index d86d3f47bd7..ae32707eb30 100644 --- a/src/vs/platform/tunnel/test/common/tunnel.test.ts +++ b/src/vs/platform/tunnel/test/common/tunnel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { extractLocalHostUriMetaDataForPortMapping, diff --git a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts index 27d69e4e5ac..32d33a7d23c 100644 --- a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts +++ b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 4c49a758185..a2561be0c11 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -55,7 +55,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun @memoize get cachePath(): Promise { const result = path.join(tmpdir(), `vscode-${this.productService.quality}-${this.productService.target}-${process.arch}`); - return pfs.Promises.mkdir(result, { recursive: true }).then(() => result); + return fs.promises.mkdir(result, { recursive: true }).then(() => result); } constructor( @@ -197,7 +197,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun const promises = versions.filter(filter).map(async one => { try { - await pfs.Promises.unlink(path.join(cachePath, one)); + await fs.promises.unlink(path.join(cachePath, one)); } catch (err) { // ignore } diff --git a/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts b/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts index 32dde9e11d7..339e86bf938 100644 --- a/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts +++ b/src/vs/platform/uriIdentity/test/common/uriIdentityService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { mock } from 'vs/base/test/common/mock'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; diff --git a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts index b6a05ce558f..8f0c5641d9f 100644 --- a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts +++ b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index b65c078f83a..f18ae097050 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -168,6 +168,10 @@ export type UserDataProfilesObject = { emptyWindows: Map; }; +type TransientUserDataProfilesObject = UserDataProfilesObject & { + folders: ResourceMap; +}; + export type StoredUserDataProfile = { name: string; location: URI; @@ -209,8 +213,9 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf private profileCreationPromises = new Map>(); - protected readonly transientProfilesObject: UserDataProfilesObject = { + protected readonly transientProfilesObject: TransientUserDataProfilesObject = { profiles: [], + folders: new ResourceMap(), workspaces: new ResourceMap(), emptyWindows: new Map() }; @@ -454,6 +459,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } async resetWorkspaces(): Promise { + this.transientProfilesObject.folders.clear(); this.transientProfilesObject.workspaces.clear(); this.transientProfilesObject.emptyWindows.clear(); this.profilesObject.workspaces.clear(); @@ -484,7 +490,17 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf getProfileForWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier): IUserDataProfile | undefined { const workspace = this.getWorkspace(workspaceIdentifier); - return URI.isUri(workspace) ? this.transientProfilesObject.workspaces.get(workspace) ?? this.profilesObject.workspaces.get(workspace) : this.transientProfilesObject.emptyWindows.get(workspace) ?? this.profilesObject.emptyWindows.get(workspace); + const profile = URI.isUri(workspace) ? this.profilesObject.workspaces.get(workspace) : this.profilesObject.emptyWindows.get(workspace); + if (profile) { + return profile; + } + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { + return this.transientProfilesObject.folders.get(workspaceIdentifier.uri); + } + if (isWorkspaceIdentifier(workspaceIdentifier)) { + return this.transientProfilesObject.workspaces.get(workspaceIdentifier.configPath); + } + return this.transientProfilesObject.emptyWindows.get(workspaceIdentifier.id); } protected getWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier): URI | string { @@ -498,16 +514,19 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } private isProfileAssociatedToWorkspace(profile: IUserDataProfile): boolean { + if ([...this.profilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { + return true; + } + if ([...this.profilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { + return true; + } if ([...this.transientProfilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { return true; } if ([...this.transientProfilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { return true; } - if ([...this.profilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { - return true; - } - if ([...this.profilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { + if ([...this.transientProfilesObject.folders.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { return true; } return false; @@ -516,6 +535,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf private updateProfiles(added: IUserDataProfile[], removed: IUserDataProfile[], updated: IUserDataProfile[]): void { const allProfiles = [...this.profiles, ...added]; const storedProfiles: StoredUserDataProfile[] = []; + const transientProfiles = this.transientProfilesObject.profiles; this.transientProfilesObject.profiles = []; for (let profile of allProfiles) { if (profile.isDefault) { @@ -525,9 +545,30 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf continue; } profile = updated.find(p => profile.id === p.id) ?? profile; + const transientProfile = transientProfiles.find(p => profile.id === p.id); if (profile.isTransient) { this.transientProfilesObject.profiles.push(profile); } else { + if (transientProfile) { + for (const [windowId, p] of this.transientProfilesObject.emptyWindows.entries()) { + if (profile.id === p.id) { + this.updateWorkspaceAssociation({ id: windowId }, profile); + break; + } + } + for (const [workspace, p] of this.transientProfilesObject.workspaces.entries()) { + if (profile.id === p.id) { + this.updateWorkspaceAssociation({ id: '', configPath: workspace }, profile); + break; + } + } + for (const [folder, p] of this.transientProfilesObject.folders.entries()) { + if (profile.id === p.id) { + this.updateWorkspaceAssociation({ id: '', uri: folder }, profile); + break; + } + } + } storedProfiles.push({ location: profile.location, name: profile.name, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); } } @@ -544,30 +585,48 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf // Force transient if the new profile to associate is transient transient = newProfile?.isTransient ? true : transient; - if (!transient) { + if (transient) { + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { + this.transientProfilesObject.folders.delete(workspaceIdentifier.uri); + if (newProfile) { + this.transientProfilesObject.folders.set(workspaceIdentifier.uri, newProfile); + } + } + + else if (isWorkspaceIdentifier(workspaceIdentifier)) { + this.transientProfilesObject.workspaces.delete(workspaceIdentifier.configPath); + if (newProfile) { + this.transientProfilesObject.workspaces.set(workspaceIdentifier.configPath, newProfile); + } + } + + else { + this.transientProfilesObject.emptyWindows.delete(workspaceIdentifier.id); + if (newProfile) { + this.transientProfilesObject.emptyWindows.set(workspaceIdentifier.id, newProfile); + } + } + } + + else { // Unset the transiet workspace association if any this.updateWorkspaceAssociation(workspaceIdentifier, undefined, true); - } + const workspace = this.getWorkspace(workspaceIdentifier); - const workspace = this.getWorkspace(workspaceIdentifier); - const profilesObject = transient ? this.transientProfilesObject : this.profilesObject; - - // Folder or Multiroot workspace - if (URI.isUri(workspace)) { - profilesObject.workspaces.delete(workspace); - if (newProfile) { - profilesObject.workspaces.set(workspace, newProfile); + // Folder or Multiroot workspace + if (URI.isUri(workspace)) { + this.profilesObject.workspaces.delete(workspace); + if (newProfile) { + this.profilesObject.workspaces.set(workspace, newProfile); + } } - } - // Empty Window - else { - profilesObject.emptyWindows.delete(workspace); - if (newProfile) { - profilesObject.emptyWindows.set(workspace, newProfile); + // Empty Window + else { + this.profilesObject.emptyWindows.delete(workspace); + if (newProfile) { + this.profilesObject.emptyWindows.set(workspace, newProfile); + } } - } - - if (!transient) { this.updateStoredProfileAssociations(); } } diff --git a/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts index a04c44f96ef..a9a7b3771c7 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, MutableDisposable, isDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, MutableDisposable, isDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IStorage, IStorageDatabase, Storage } from 'vs/base/parts/storage/common/storage'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { AbstractStorageService, IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget, isProfileUsingDefaultStorage } from 'vs/platform/storage/common/storage'; @@ -63,10 +63,16 @@ export abstract class AbstractUserDataProfileStorageService extends Disposable i readonly abstract onDidChange: Event; + private readonly storageServicesMap: DisposableMap | undefined; + constructor( + persistStorages: boolean, @IStorageService protected readonly storageService: IStorageService ) { super(); + if (persistStorages) { + this.storageServicesMap = this._register(new DisposableMap()); + } } async readStorageData(profile: IUserDataProfile): Promise> { @@ -82,16 +88,30 @@ export abstract class AbstractUserDataProfileStorageService extends Disposable i return fn(this.storageService); } - const storageDatabase = await this.createStorageDatabase(profile); - const storageService = new StorageService(storageDatabase); + let storageService = this.storageServicesMap?.get(profile.id); + if (!storageService) { + storageService = new StorageService(this.createStorageDatabase(profile)); + this.storageServicesMap?.set(profile.id, storageService); + + try { + await storageService.initialize(); + } catch (error) { + if (this.storageServicesMap?.has(profile.id)) { + this.storageServicesMap.deleteAndDispose(profile.id); + } else { + storageService.dispose(); + } + throw error; + } + } try { - await storageService.initialize(); const result = await fn(storageService); await storageService.flush(); return result; } finally { - storageService.dispose(); - await this.closeAndDispose(storageDatabase); + if (!this.storageServicesMap?.has(profile.id)) { + storageService.dispose(); + } } } @@ -111,16 +131,6 @@ export abstract class AbstractUserDataProfileStorageService extends Disposable i storageService.storeAll(Array.from(items.entries()).map(([key, value]) => ({ key, value, scope: StorageScope.PROFILE, target })), true); } - protected async closeAndDispose(storageDatabase: IStorageDatabase): Promise { - try { - await storageDatabase.close(); - } finally { - if (isDisposable(storageDatabase)) { - storageDatabase.dispose(); - } - } - } - protected abstract createStorageDatabase(profile: IUserDataProfile): Promise; } @@ -130,12 +140,13 @@ export class RemoteUserDataProfileStorageService extends AbstractUserDataProfile readonly onDidChange: Event; constructor( + persistStorages: boolean, private readonly remoteService: IRemoteService, userDataProfilesService: IUserDataProfilesService, storageService: IStorageService, logService: ILogService, ) { - super(storageService); + super(persistStorages, storageService); const channel = remoteService.getChannel('profileStorageListener'); const disposable = this._register(new MutableDisposable()); @@ -164,14 +175,26 @@ export class RemoteUserDataProfileStorageService extends AbstractUserDataProfile class StorageService extends AbstractStorageService { - private readonly profileStorage: IStorage; + private profileStorage: IStorage | undefined; - constructor(profileStorageDatabase: IStorageDatabase) { + constructor(private readonly profileStorageDatabase: Promise) { super({ flushInterval: 100 }); - this.profileStorage = this._register(new Storage(profileStorageDatabase)); } - protected doInitialize(): Promise { + protected async doInitialize(): Promise { + const profileStorageDatabase = await this.profileStorageDatabase; + const profileStorage = new Storage(profileStorageDatabase); + this._register(profileStorage.onDidChangeStorage(e => { + this.emitDidChangeValue(StorageScope.PROFILE, e); + })); + this._register(toDisposable(() => { + profileStorage.close(); + profileStorage.dispose(); + if (isDisposable(profileStorageDatabase)) { + profileStorageDatabase.dispose(); + } + })); + this.profileStorage = profileStorage; return this.profileStorage.init(); } diff --git a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts index 0679efcc742..313bedc9bff 100644 --- a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts +++ b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts @@ -18,7 +18,7 @@ export class NativeUserDataProfileStorageService extends RemoteUserDataProfileSt @IStorageService storageService: IStorageService, @ILogService logService: ILogService, ) { - super(mainProcessService, userDataProfilesService, storageService, logService); + super(false, mainProcessService, userDataProfilesService, storageService, logService); } } diff --git a/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts index 3b37d056aae..703011c9d60 100644 --- a/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts +++ b/src/vs/platform/userDataProfile/node/userDataProfileStorageService.ts @@ -9,7 +9,7 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; import { RemoteUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; -export class NativeUserDataProfileStorageService extends RemoteUserDataProfileStorageService { +export class SharedProcessUserDataProfileStorageService extends RemoteUserDataProfileStorageService { constructor( @IMainProcessService mainProcessService: IMainProcessService, @@ -17,6 +17,6 @@ export class NativeUserDataProfileStorageService extends RemoteUserDataProfileSt @IStorageService storageService: IStorageService, @ILogService logService: ILogService, ) { - super(mainProcessService, userDataProfilesService, storageService, logService); + super(true, mainProcessService, userDataProfilesService, storageService, logService); } } diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts index 7e898dd15fa..a7e2da87458 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts index 48a68ee98a1..36cfb4efce5 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { InMemoryStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage'; @@ -43,7 +43,6 @@ export class TestUserDataProfileStorageService extends AbstractUserDataProfileSt return this.createStorageDatabase(profile); } - protected override async closeAndDispose(): Promise { } } suite('ProfileStorageService', () => { @@ -54,7 +53,7 @@ suite('ProfileStorageService', () => { let storage: Storage; setup(async () => { - testObject = disposables.add(new TestUserDataProfileStorageService(disposables.add(new InMemoryStorageService()))); + testObject = disposables.add(new TestUserDataProfileStorageService(false, disposables.add(new InMemoryStorageService()))); storage = disposables.add(new Storage(await testObject.setupStorageDatabase(profile))); await storage.init(); }); diff --git a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts index 515b2ce80da..8797fafc6c4 100644 --- a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts +++ b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 53561e249db..7af2df9134a 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -533,7 +533,7 @@ export class LocalExtensionsProvider { addToSkipped.push(e); this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension`, gallery.displayName || gallery.identifier.id); } - if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) { + if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleApi, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) { this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, gallery.displayName || gallery.identifier.id); } else if (error) { this.logService.error(error); diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index b1c7523e8ef..c9499d33e9e 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { distinct } from 'vs/base/common/arrays'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; @@ -12,6 +13,7 @@ import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configur import { ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -19,7 +21,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { getIgnoredSettings, isEmpty, merge, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; -import { Change, IRemoteUserData, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, IRemoteUserData, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest, getIgnoredSettingsForExtension } from 'vs/platform/userDataSync/common/userDataSync'; interface ISettingsResourcePreview extends IFileResourcePreview { previewResult: IMergeResult; @@ -51,7 +53,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }); constructor( - profile: IUserDataProfile, + private readonly profile: IUserDataProfile, collection: string | undefined, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @@ -315,21 +317,39 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return { settings }; } - private _defaultIgnoredSettings: Promise | undefined = undefined; + private coreIgnoredSettings: Promise | undefined = undefined; + private systemExtensionsIgnoredSettings: Promise | undefined = undefined; + private userExtensionsIgnoredSettings: Promise | undefined = undefined; private async getIgnoredSettings(content?: string): Promise { - if (!this._defaultIgnoredSettings) { - this._defaultIgnoredSettings = this.userDataSyncUtilService.resolveDefaultIgnoredSettings(); + if (!this.coreIgnoredSettings) { + this.coreIgnoredSettings = this.userDataSyncUtilService.resolveDefaultCoreIgnoredSettings(); + } + if (!this.systemExtensionsIgnoredSettings) { + this.systemExtensionsIgnoredSettings = this.getIgnoredSettingForSystemExtensions(); + } + if (!this.userExtensionsIgnoredSettings) { + this.userExtensionsIgnoredSettings = this.getIgnoredSettingForUserExtensions(); const disposable = this._register(Event.any( Event.filter(this.extensionManagementService.onDidInstallExtensions, (e => e.some(({ local }) => !!local))), Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)))(() => { disposable.dispose(); - this._defaultIgnoredSettings = undefined; + this.userExtensionsIgnoredSettings = undefined; })); } - const defaultIgnoredSettings = await this._defaultIgnoredSettings; + const defaultIgnoredSettings = (await Promise.all([this.coreIgnoredSettings, this.systemExtensionsIgnoredSettings, this.userExtensionsIgnoredSettings])).flat(); return getIgnoredSettings(defaultIgnoredSettings, this.configurationService, content); } + private async getIgnoredSettingForSystemExtensions(): Promise { + const systemExtensions = await this.extensionManagementService.getInstalled(ExtensionType.System); + return distinct(systemExtensions.map(e => getIgnoredSettingsForExtension(e.manifest)).flat()); + } + + private async getIgnoredSettingForUserExtensions(): Promise { + const userExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User, this.profile.extensionsResource); + return distinct(userExtensions.map(e => getIgnoredSettingsForExtension(e.manifest)).flat()); + } + private validateContent(content: string): void { if (this.hasErrors(content, false)) { throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 084755420cb..ed81c9196e8 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -15,9 +15,10 @@ import { isObject, isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IHeaders } from 'vs/base/parts/request/common/request'; import { localize } from 'vs/nls'; -import { allSettings, ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { allSettings, ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry, IRegisteredConfigurationPropertySchema, getAllConfigurationProperties, parseScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { EXTENSION_IDENTIFIER_PATTERN, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { ILogService } from 'vs/platform/log/common/log'; @@ -30,12 +31,40 @@ export function getDisallowedIgnoredSettings(): string[] { return Object.keys(allSettings).filter(setting => !!allSettings[setting].disallowSyncIgnore); } -export function getDefaultIgnoredSettings(): string[] { +export function getDefaultIgnoredSettings(excludeExtensions: boolean = false): string[] { const allSettings = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); - const ignoreSyncSettings = Object.keys(allSettings).filter(setting => !!allSettings[setting].ignoreSync); - const machineSettings = Object.keys(allSettings).filter(setting => allSettings[setting].scope === ConfigurationScope.MACHINE || allSettings[setting].scope === ConfigurationScope.MACHINE_OVERRIDABLE); + const ignoredSettings = getIgnoredSettings(allSettings, excludeExtensions); const disallowedSettings = getDisallowedIgnoredSettings(); - return distinct([...ignoreSyncSettings, ...machineSettings, ...disallowedSettings]); + return distinct([...ignoredSettings, ...disallowedSettings]); +} + +export function getIgnoredSettingsForExtension(manifest: IExtensionManifest): string[] { + if (!manifest.contributes?.configuration) { + return []; + } + const configurations = Array.isArray(manifest.contributes.configuration) ? manifest.contributes.configuration : [manifest.contributes.configuration]; + if (!configurations.length) { + return []; + } + const properties = getAllConfigurationProperties(configurations); + return getIgnoredSettings(properties, false); +} + +function getIgnoredSettings(properties: IStringDictionary, excludeExtensions: boolean): string[] { + const ignoredSettings = new Set(); + for (const key in properties) { + if (excludeExtensions && !!properties[key].source) { + continue; + } + const scope = isString(properties[key].scope) ? parseScope(properties[key].scope) : properties[key].scope; + if (properties[key].ignoreSync + || scope === ConfigurationScope.MACHINE + || scope === ConfigurationScope.MACHINE_OVERRIDABLE + ) { + ignoredSettings.add(key); + } + } + return [...ignoredSettings.values()]; } export const USER_DATA_SYNC_CONFIGURATION_SCOPE = 'settingsSync'; @@ -591,7 +620,7 @@ export interface IUserDataSyncUtilService { readonly _serviceBrand: undefined; resolveUserBindings(userbindings: string[]): Promise>; resolveFormattingOptions(resource: URI): Promise; - resolveDefaultIgnoredSettings(): Promise; + resolveDefaultCoreIgnoredSettings(): Promise; } export const IUserDataSyncLogService = createDecorator('IUserDataSyncLogService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncLocalStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncLocalStoreService.ts index 28d2400cfee..8c505b3aec7 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncLocalStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncLocalStoreService.ts @@ -53,7 +53,7 @@ export class UserDataSyncLocalStoreService extends Disposable implements IUserDa if (stat.children) { for (const child of stat.children) { - if (child.isDirectory && !this.userDataProfilesService.profiles.some(profile => profile.id === child.name)) { + if (child.isDirectory && !ALL_SYNC_RESOURCES.includes(child.name) && !this.userDataProfilesService.profiles.some(profile => profile.id === child.name)) { try { this.logService.info('Deleting non existing profile from backup', child.resource.path); await this.fileService.del(child.resource, { recursive: true }); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 856003580d0..a11d2cb04cc 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -702,8 +702,7 @@ class ProfileSynchronizer extends Disposable { const [[synchronizer, , disposable]] = this._enabled.splice(index, 1); disposable.dispose(); this.updateStatus(); - Promise.allSettled([synchronizer.stop(), synchronizer.resetLocal()]) - .then(null, error => this.logService.error(error)); + synchronizer.stop().then(null, error => this.logService.error(error)); } } diff --git a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts index 3436a31fb07..1ecc7b900a4 100644 --- a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; import { ILocalSyncExtension, ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync'; diff --git a/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts b/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts index 3c61cd98aca..7a17fac286d 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; diff --git a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts index d27a2d8d6ac..7d3ecaca732 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; diff --git a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts index 935c553a872..c7a1f8675e7 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; import { TestUserDataSyncUtilService } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts index 4799201077a..660185fd8fd 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IFileService } from 'vs/platform/files/common/files'; diff --git a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts index 274ac5ee696..625df21215c 100644 --- a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { addSetting, merge, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import type { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync'; diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index 0cd110208b8..39246f25ce1 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts index f41039cbb9e..50e5caa545f 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { merge } from 'vs/platform/userDataSync/common/snippetsMerge'; diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index fc9644c2245..97f4ab83f13 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { IStringDictionary } from 'vs/base/common/collections'; import { dirname, joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 1e854038a9d..a336ca32411 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Barrier } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; diff --git a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts index 73fe0a6b23d..c6cfd18a44b 100644 --- a/src/vs/platform/userDataSync/test/common/tasksSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/tasksSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 47cde8003c7..e9c86afd301 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts index 40cc6f3623d..e60f5314799 100644 --- a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IUserDataProfile, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; diff --git a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts index 2d6d7de2bf0..2227e3d1182 100644 --- a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index b622a3efe44..3cd97bda585 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -26,7 +26,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IRequestService } from 'vs/platform/request/common/request'; +import { AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; @@ -95,7 +95,7 @@ export class UserDataSyncClient extends Disposable { const storageService = this._register(new TestStorageService(userDataProfilesService.defaultProfile)); this.instantiationService.stub(IStorageService, this._register(storageService)); - this.instantiationService.stub(IUserDataProfileStorageService, this._register(new TestUserDataProfileStorageService(storageService))); + this.instantiationService.stub(IUserDataProfileStorageService, this._register(new TestUserDataProfileStorageService(false, storageService))); const configurationService = this._register(new ConfigurationService(userDataProfilesService.defaultProfile.settingsResource, fileService, new NullPolicyService(), logService)); await configurationService.initialize(); @@ -188,6 +188,7 @@ export class UserDataSyncTestServer implements IRequestService { constructor(private readonly rateLimit = Number.MAX_SAFE_INTEGER, private readonly retryAfter?: number) { } async resolveProxy(url: string): Promise { return url; } + async lookupAuthorization(authInfo: AuthInfo): Promise { return undefined; } async loadCertificates(): Promise { return []; } async request(options: IRequestOptions, token: CancellationToken): Promise { @@ -355,7 +356,7 @@ export class TestUserDataSyncUtilService implements IUserDataSyncUtilService { _serviceBrand: any; - async resolveDefaultIgnoredSettings(): Promise { + async resolveDefaultCoreIgnoredSettings(): Promise { return getDefaultIgnoredSettings(); } diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 665d09ed41e..a06d711277e 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { dirname, joinPath } from 'vs/base/common/resources'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index db83d62e163..03f68a73da9 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { newWriteableBufferStream } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -414,6 +414,7 @@ suite('UserDataSyncRequestsSession', () => { _serviceBrand: undefined, async request() { return { res: { headers: {} }, stream: newWriteableBufferStream() }; }, async resolveProxy() { return undefined; }, + async lookupAuthorization() { return undefined; }, async loadCertificates() { return []; } }; diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts index 70be47e3b27..4888e76a2fe 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts @@ -89,6 +89,7 @@ export class UtilityProcessWorkerMainService extends Disposable implements IUtil this.logService.trace(`[UtilityProcessWorker]: disposeWorker(window: ${configuration.reply.windowId}, moduleId: ${configuration.process.moduleId})`); worker.kill(); + worker.dispose(); this.workers.delete(workerId); } } @@ -98,7 +99,7 @@ class UtilityProcessWorker extends Disposable { private readonly _onDidTerminate = this._register(new Emitter()); readonly onDidTerminate = this._onDidTerminate.event; - private readonly utilityProcess = new WindowUtilityProcess(this.logService, this.windowsMainService, this.telemetryService, this.lifecycleMainService); + private readonly utilityProcess = this._register(new WindowUtilityProcess(this.logService, this.windowsMainService, this.telemetryService, this.lifecycleMainService)); constructor( @ILogService private readonly logService: ILogService, diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index ae20d9d7620..2b7ffc4651f 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -49,6 +49,9 @@ export interface IBaseOpenWindowsOptions { * If not set, defaults to the remote authority of the current window. */ readonly remoteAuthority?: string | null; + + readonly forceProfile?: string; + readonly forceTempProfile?: boolean; } export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { @@ -64,9 +67,6 @@ export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { readonly gotoLineMode?: boolean; readonly waitMarkerFileURI?: URI; - - readonly forceProfile?: string; - readonly forceTempProfile?: boolean; } export interface IAddFoldersRequest { @@ -158,6 +158,7 @@ export interface IWindowSettings { readonly enableMenuBarMnemonics: boolean; readonly closeWhenEmpty: boolean; readonly clickThroughInactive: boolean; + readonly newWindowProfile: string; readonly density: IDensitySettings; } diff --git a/src/vs/platform/window/electron-main/window.ts b/src/vs/platform/window/electron-main/window.ts index b63a6117514..15fcb1790fe 100644 --- a/src/vs/platform/window/electron-main/window.ts +++ b/src/vs/platform/window/electron-main/window.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, Rectangle, screen, WebContents } from 'electron'; +import electron from 'electron'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -23,7 +23,7 @@ export interface IBaseWindow extends IDisposable { readonly onDidClose: Event; readonly id: number; - readonly win: BrowserWindow | null; + readonly win: electron.BrowserWindow | null; readonly lastFocusTime: number; focus(options?: { force: boolean }): void; @@ -41,7 +41,7 @@ export interface IBaseWindow extends IDisposable { updateWindowControls(options: { height?: number; backgroundColor?: string; foregroundColor?: string }): void; - matches(webContents: WebContents): boolean; + matches(webContents: electron.WebContents): boolean; } export interface ICodeWindow extends IBaseWindow { @@ -76,7 +76,7 @@ export interface ICodeWindow extends IBaseWindow { close(): void; - getBounds(): Rectangle; + getBounds(): electron.Rectangle; send(channel: string, ...args: any[]): void; sendWhenReady(channel: string, token: CancellationToken, ...args: any[]): void; @@ -158,7 +158,7 @@ export const defaultAuxWindowState = function (): IWindowState { const width = 800; const height = 600; - const workArea = screen.getPrimaryDisplay().workArea; + const workArea = electron.screen.getPrimaryDisplay().workArea; const x = Math.max(workArea.x + (workArea.width / 2) - (width / 2), 0); const y = Math.max(workArea.y + (workArea.height / 2) - (height / 2), 0); diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index dcb3a1776ec..0f626a7ddfc 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, Display, nativeImage, NativeImage, Rectangle, screen, SegmentedControlSegment, systemPreferences, TouchBar, TouchBarSegmentedControl, WebContents, Event as ElectronEvent } from 'electron'; +import electron from 'electron'; import { DeferredPromise, RunOnceScheduler, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -51,7 +51,7 @@ export interface IWindowCreationOptions { readonly isExtensionTestHost?: boolean; } -interface ITouchBarSegment extends SegmentedControlSegment { +interface ITouchBarSegment extends electron.SegmentedControlSegment { readonly id: string; } @@ -111,9 +111,9 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { protected _lastFocusTime = Date.now(); // window is shown on creation so take current time get lastFocusTime(): number { return this._lastFocusTime; } - protected _win: BrowserWindow | null = null; + protected _win: electron.BrowserWindow | null = null; get win() { return this._win; } - protected setWin(win: BrowserWindow): void { + protected setWin(win: electron.BrowserWindow): void { this._win = win; // Window Events @@ -160,7 +160,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { // This sets up a listener for the window hook. This is a Windows-only API provided by electron. win.hookWindowMessage(WM_INITMENU, () => { const [x, y] = win.getPosition(); - const cursorPos = screen.getCursorScreenPoint(); + const cursorPos = electron.screen.getCursorScreenPoint(); const cx = cursorPos.x - x; const cy = cursorPos.y - y; @@ -218,7 +218,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { super(); } - protected applyState(state: IWindowState, hasMultipleDisplays = screen.getAllDisplays().length > 0): void { + protected applyState(state: IWindowState, hasMultipleDisplays = electron.screen.getAllDisplays().length > 0): void { // TODO@electron (Electron 4 regression): when running on multiple displays where the target display // to open the window has a larger resolution than the primary display, the window will not size @@ -232,7 +232,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { const windowSettings = this.configurationService.getValue('window'); const useNativeTabs = isMacintosh && windowSettings?.nativeTabs === true; - if ((isMacintosh || isWindows) && hasMultipleDisplays && (!useNativeTabs || BrowserWindow.getAllWindows().length === 1)) { + if ((isMacintosh || isWindows) && hasMultipleDisplays && (!useNativeTabs || electron.BrowserWindow.getAllWindows().length === 1)) { if ([state.width, state.height, state.x, state.y].every(value => typeof value === 'number')) { this._win?.setBounds({ width: state.width, @@ -299,7 +299,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { focus(options?: { force: boolean }): void { if (isMacintosh && options?.force) { - app.focus({ steal: true }); + electron.app.focus({ steal: true }); } const win = this.win; @@ -322,7 +322,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { // Respect system settings on mac with regards to title click on windows title if (isMacintosh) { - const action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string'); + const action = electron.systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string'); switch (action) { case 'Minimize': win.minimize(); @@ -494,7 +494,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { //#endregion - abstract matches(webContents: WebContents): boolean; + abstract matches(webContents: electron.WebContents): boolean; override dispose(): void { super.dispose(); @@ -524,7 +524,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private _id: number; get id(): number { return this._id; } - protected override _win: BrowserWindow; + protected override _win: electron.BrowserWindow; get backupPath(): string | undefined { return this._config?.backupPath; } @@ -561,7 +561,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[] = []; - private readonly touchBarGroups: TouchBarSegmentedControl[] = []; + private readonly touchBarGroups: electron.TouchBarSegmentedControl[] = []; private currentHttpProxy: string | undefined = undefined; private currentNoProxy: string | undefined = undefined; @@ -612,7 +612,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // Create the browser window mark('code/willCreateCodeBrowserWindow'); - this._win = new BrowserWindow(options); + this._win = new electron.BrowserWindow(options); mark('code/didCreateCodeBrowserWindow'); this._id = this._win.id; @@ -693,7 +693,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // unloading a window that should not be confused // with the DOM way. // (https://github.com/microsoft/vscode/issues/122736) - this._register(Event.fromNodeEventEmitter(this._win.webContents, 'will-prevent-unload')(event => event.preventDefault())); + this._register(Event.fromNodeEventEmitter(this._win.webContents, 'will-prevent-unload')(event => event.preventDefault())); // Remember that we loaded this._register(Event.fromNodeEventEmitter(this._win.webContents, 'did-finish-load')(() => { @@ -957,16 +957,25 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } // Proxy - if (!e || e.affectsConfiguration('http.proxy')) { + if (!e || e.affectsConfiguration('http.proxy') || e.affectsConfiguration('http.noProxy')) { let newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() || (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() // Not standardized. || undefined; + if (newHttpProxy?.indexOf('@') !== -1) { + const uri = URI.parse(newHttpProxy!); + const i = uri.authority.indexOf('@'); + if (i !== -1) { + newHttpProxy = uri.with({ authority: uri.authority.substring(i + 1) }) + .toString(); + } + } if (newHttpProxy?.endsWith('/')) { newHttpProxy = newHttpProxy.substr(0, newHttpProxy.length - 1); } - const newNoProxy = (process.env['no_proxy'] || process.env['NO_PROXY'] || '').trim() || undefined; // Not standardized. + const newNoProxy = (this.configurationService.getValue('http.noProxy') || []).map((item) => item.trim()).join(',') + || (process.env['no_proxy'] || process.env['NO_PROXY'] || '').trim() || undefined; // Not standardized. if ((newHttpProxy || '').indexOf('@') === -1 && (newHttpProxy !== this.currentHttpProxy || newNoProxy !== this.currentNoProxy)) { this.currentHttpProxy = newHttpProxy; this.currentNoProxy = newNoProxy; @@ -975,13 +984,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { const proxyBypassRules = newNoProxy ? `${newNoProxy},` : ''; this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`); this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); - type appWithProxySupport = Electron.App & { - setProxy(config: Electron.Config): Promise; - resolveProxy(url: string): Promise; - }; - if (typeof (app as appWithProxySupport).setProxy === 'function') { - (app as appWithProxySupport).setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); - } + electron.app.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); } } } @@ -1132,7 +1135,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { configuration['extensions-dir'] = cli['extensions-dir']; } - configuration.accessibilitySupport = app.isAccessibilitySupportEnabled(); + configuration.accessibilitySupport = electron.app.isAccessibilitySupportEnabled(); configuration.isInitialStartup = false; // since this is a reload configuration.policiesData = this.policyService.serialize(); // set policies data again configuration.continueOn = this.environmentMainService.continueOn; @@ -1186,9 +1189,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // fullscreen gets special treatment if (this.isFullScreen) { - let display: Display | undefined; + let display: electron.Display | undefined; try { - display = screen.getDisplayMatching(this.getBounds()); + display = electron.screen.getDisplayMatching(this.getBounds()); } catch (error) { // Electron has weird conditions under which it throws errors // e.g. https://github.com/microsoft/vscode/issues/100334 when @@ -1233,7 +1236,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // only consider non-minimized window states if (mode === WindowMode.Normal || mode === WindowMode.Maximized) { - let bounds: Rectangle; + let bounds: electron.Rectangle; if (mode === WindowMode.Normal) { bounds = this.getBounds(); } else { @@ -1262,7 +1265,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // Window dimensions try { - const displays = screen.getAllDisplays(); + const displays = electron.screen.getAllDisplays(); hasMultipleDisplays = displays.length > 1; state = WindowStateValidator.validateWindowState(this.logService, state, displays); @@ -1276,7 +1279,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { return [state || defaultWindowState(), hasMultipleDisplays]; } - getBounds(): Rectangle { + getBounds(): electron.Rectangle { const [x, y] = this._win.getPosition(); const [width, height] = this._win.getSize(); @@ -1425,16 +1428,16 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this.touchBarGroups.push(groupTouchBar); } - this._win.setTouchBar(new TouchBar({ items: this.touchBarGroups })); + this._win.setTouchBar(new electron.TouchBar({ items: this.touchBarGroups })); } - private createTouchBarGroup(items: ISerializableCommandAction[] = []): TouchBarSegmentedControl { + private createTouchBarGroup(items: ISerializableCommandAction[] = []): electron.TouchBarSegmentedControl { // Group Segments const segments = this.createTouchBarGroupSegments(items); // Group Control - const control = new TouchBar.TouchBarSegmentedControl({ + const control = new electron.TouchBar.TouchBarSegmentedControl({ segments, mode: 'buttons', segmentStyle: 'automatic', @@ -1448,9 +1451,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private createTouchBarGroupSegments(items: ISerializableCommandAction[] = []): ITouchBarSegment[] { const segments: ITouchBarSegment[] = items.map(item => { - let icon: NativeImage | undefined; + let icon: electron.NativeImage | undefined; if (item.icon && !ThemeIcon.isThemeIcon(item.icon) && item.icon?.dark?.scheme === Schemas.file) { - icon = nativeImage.createFromPath(URI.revive(item.icon.dark).fsPath); + icon = electron.nativeImage.createFromPath(URI.revive(item.icon.dark).fsPath); if (icon.isEmpty()) { icon = undefined; } @@ -1473,7 +1476,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { return segments; } - matches(webContents: WebContents): boolean { + matches(webContents: electron.WebContents): boolean { return this._win?.webContents.id === webContents.id; } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index abfc98bc2a3..30ed4fe16b0 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindowConstructorOptions, Display, Rectangle, WebContents, WebPreferences, screen } from 'electron'; +import electron from 'electron'; import { Event } from 'vs/base/common/event'; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; @@ -53,7 +53,7 @@ export interface IWindowsMainService { getLastActiveWindow(): ICodeWindow | undefined; getWindowById(windowId: number): ICodeWindow | undefined; - getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined; + getWindowByWebContents(webContents: electron.WebContents): ICodeWindow | undefined; } export interface IWindowsCountChangedEvent { @@ -115,7 +115,7 @@ export interface IOpenConfiguration extends IBaseOpenConfiguration { export interface IOpenEmptyConfiguration extends IBaseOpenConfiguration { } -export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowState: IWindowState, webPreferences?: WebPreferences): BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } { +export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowState: IWindowState, webPreferences?: electron.WebPreferences): electron.BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } { const themeMainService = accessor.get(IThemeMainService); const productService = accessor.get(IProductService); const configurationService = accessor.get(IConfigurationService); @@ -123,7 +123,7 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt const windowSettings = configurationService.getValue('window'); - const options: BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } = { + const options: electron.BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } = { backgroundColor: themeMainService.getBackgroundColor(), minWidth: WindowMinimumSize.WIDTH, minHeight: WindowMinimumSize.HEIGHT, @@ -215,7 +215,7 @@ export function getLastFocused(windows: ICodeWindow[] | IAuxiliaryWindow[]): ICo export namespace WindowStateValidator { - export function validateWindowState(logService: ILogService, state: IWindowState, displays = screen.getAllDisplays()): IWindowState | undefined { + export function validateWindowState(logService: ILogService, state: IWindowState, displays = electron.screen.getAllDisplays()): IWindowState | undefined { logService.trace(`window#validateWindowState: validating window state on ${displays.length} display(s)`, state); if ( @@ -313,10 +313,10 @@ export namespace WindowStateValidator { } // Multi Monitor (non-fullscreen): ensure window is within display bounds - let display: Display | undefined; - let displayWorkingArea: Rectangle | undefined; + let display: electron.Display | undefined; + let displayWorkingArea: electron.Rectangle | undefined; try { - display = screen.getDisplayMatching({ x: state.x, y: state.y, width: state.width, height: state.height }); + display = electron.screen.getDisplayMatching({ x: state.x, y: state.y, width: state.width, height: state.height }); displayWorkingArea = getWorkingArea(display); logService.trace('window#validateWindowState: multi-monitor working area', displayWorkingArea); @@ -343,7 +343,7 @@ export namespace WindowStateValidator { return undefined; } - function getWorkingArea(display: Display): Rectangle | undefined { + function getWorkingArea(display: electron.Display): electron.Rectangle | undefined { // Prefer the working area of the display to account for taskbars on the // desktop being positioned somewhere (https://github.com/microsoft/vscode/issues/50830). diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index a4cb0ca698b..070c7ae05d3 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { app, BrowserWindow, WebContents, shell } from 'electron'; -import { Promises } from 'vs/base/node/pfs'; import { addUNCHostToAllowlist } from 'vs/base/node/unc'; import { hostname, release, arch } from 'os'; import { coalesce, distinct } from 'vs/base/common/arrays'; @@ -269,7 +269,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const forceReuseWindow = options?.forceReuseWindow; const forceNewWindow = !forceReuseWindow; - return this.open({ ...openConfig, cli, forceEmpty, forceNewWindow, forceReuseWindow, remoteAuthority }); + return this.open({ ...openConfig, cli, forceEmpty, forceNewWindow, forceReuseWindow, remoteAuthority, forceTempProfile: options?.forceTempProfile, forceProfile: options?.forceProfile }); } openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void { @@ -1057,7 +1057,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic path = sanitizeFilePath(normalize(path), cwd()); try { - const pathStat = await Promises.stat(path); + const pathStat = await fs.promises.stat(path); // File if (pathStat.isFile()) { @@ -1390,7 +1390,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const windowConfig = this.configurationService.getValue('window'); const lastActiveWindow = this.getLastActiveWindow(); - const defaultProfile = lastActiveWindow?.profile ?? this.userDataProfilesMainService.defaultProfile; + const newWindowProfile = windowConfig?.newWindowProfile + ? this.userDataProfilesMainService.profiles.find(profile => profile.name === windowConfig.newWindowProfile) : undefined; + const defaultProfile = newWindowProfile ?? lastActiveWindow?.profile ?? this.userDataProfilesMainService.defaultProfile; let window: ICodeWindow | undefined; if (!options.forceNewWindow && !options.forceNewTabbedWindow) { @@ -1442,6 +1444,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic workspace: options.workspace, userEnv: { ...this.initialUserEnv, ...options.userEnv }, + nls: { + // VSCODE_GLOBALS: NLS + messages: globalThis._VSCODE_NLS_MESSAGES, + language: globalThis._VSCODE_NLS_LANGUAGE + }, + filesToOpenOrCreate: options.filesToOpen?.filesToOpenOrCreate, filesToDiff: options.filesToOpen?.filesToDiff, filesToMerge: options.filesToOpen?.filesToMerge, diff --git a/src/vs/platform/windows/electron-main/windowsStateHandler.ts b/src/vs/platform/windows/electron-main/windowsStateHandler.ts index df866ea3556..6a6591cd44d 100644 --- a/src/vs/platform/windows/electron-main/windowsStateHandler.ts +++ b/src/vs/platform/windows/electron-main/windowsStateHandler.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, Display, screen } from 'electron'; +import electron from 'electron'; import { Disposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; @@ -78,26 +78,26 @@ export class WindowsStateHandler extends Disposable { // When a window looses focus, save all windows state. This allows to // prevent loss of window-state data when OS is restarted without properly // shutting down the application (https://github.com/microsoft/vscode/issues/87171) - app.on('browser-window-blur', () => { + electron.app.on('browser-window-blur', () => { if (!this.shuttingDown) { this.saveWindowsState(); } }); // Handle various lifecycle events around windows - this.lifecycleMainService.onBeforeCloseWindow(window => this.onBeforeCloseWindow(window)); - this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); - this.windowsMainService.onDidChangeWindowsCount(e => { + this._register(this.lifecycleMainService.onBeforeCloseWindow(window => this.onBeforeCloseWindow(window))); + this._register(this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown())); + this._register(this.windowsMainService.onDidChangeWindowsCount(e => { if (e.newCount - e.oldCount > 0) { // clear last closed window state when a new window opens. this helps on macOS where // otherwise closing the last window, opening a new window and then quitting would // use the state of the previously closed window when restarting. this.lastClosedState = undefined; } - }); + })); // try to save state before destroy because close will not fire - this.windowsMainService.onDidDestroyWindow(window => this.onBeforeCloseWindow(window)); + this._register(this.windowsMainService.onDidDestroyWindow(window => this.onBeforeCloseWindow(window))); } // Note that onBeforeShutdown() and onBeforeCloseWindow() are fired in different order depending on the OS: @@ -339,8 +339,8 @@ export class WindowsStateHandler extends Disposable { // // We want the new window to open on the same display that the last active one is in - let displayToUse: Display | undefined; - const displays = screen.getAllDisplays(); + let displayToUse: electron.Display | undefined; + const displays = electron.screen.getAllDisplays(); // Single Display if (displays.length === 1) { @@ -352,18 +352,18 @@ export class WindowsStateHandler extends Disposable { // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is if (isMacintosh) { - const cursorPoint = screen.getCursorScreenPoint(); - displayToUse = screen.getDisplayNearestPoint(cursorPoint); + const cursorPoint = electron.screen.getCursorScreenPoint(); + displayToUse = electron.screen.getDisplayNearestPoint(cursorPoint); } // if we have a last active window, use that display for the new window if (!displayToUse && lastActive) { - displayToUse = screen.getDisplayMatching(lastActive.getBounds()); + displayToUse = electron.screen.getDisplayMatching(lastActive.getBounds()); } // fallback to primary display or first display if (!displayToUse) { - displayToUse = screen.getPrimaryDisplay() || displays[0]; + displayToUse = electron.screen.getPrimaryDisplay() || displays[0]; } } diff --git a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index 490d6858a62..f3ce84f9c7a 100644 --- a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { join } from 'vs/base/common/path'; diff --git a/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts index 0b96b1bf740..65d1a9937f9 100644 --- a/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/workspace/test/common/workspace.test.ts b/src/vs/platform/workspace/test/common/workspace.test.ts index 2464d5e4a35..fb8c1cf18e6 100644 --- a/src/vs/platform/workspace/test/common/workspace.test.ts +++ b/src/vs/platform/workspace/test/common/workspace.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { join } from 'vs/base/common/path'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts index 236f6d6fc32..bfb4fcd5145 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow } from 'electron'; +import * as fs from 'fs'; +import electron from 'electron'; import { Emitter, Event } from 'vs/base/common/event'; import { parse } from 'vs/base/common/json'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -102,7 +103,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork } resolveLocalWorkspace(uri: URI): Promise { - return this.doResolveLocalWorkspace(uri, path => Promises.readFile(path, 'utf8')); + return this.doResolveLocalWorkspace(uri, path => fs.promises.readFile(path, 'utf8')); } private doResolveLocalWorkspace(uri: URI, contentsFn: (path: string) => string): IResolvedWorkspace | undefined; @@ -169,7 +170,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); const configPath = workspace.configPath.fsPath; - await Promises.mkdir(dirname(configPath), { recursive: true }); + await fs.promises.mkdir(dirname(configPath), { recursive: true }); await Promises.writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); this.untitledWorkspaces.push({ workspace, remoteAuthority }); @@ -280,7 +281,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork buttons: [localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK")], message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(workspacePath)), detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again.") - }, BrowserWindow.getFocusedWindow() ?? undefined); + }, electron.BrowserWindow.getFocusedWindow() ?? undefined); return false; } diff --git a/src/vs/platform/workspaces/test/common/workspaces.test.ts b/src/vs/platform/workspaces/test/common/workspaces.test.ts index a1f2f262d00..a08f1035ce1 100644 --- a/src/vs/platform/workspaces/test/common/workspaces.test.ts +++ b/src/vs/platform/workspaces/test/common/workspaces.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISerializedSingleFolderWorkspaceIdentifier, ISerializedWorkspaceIdentifier, reviveIdentifier, hasWorkspaceFileExtension, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IEmptyWorkspaceIdentifier, toWorkspaceIdentifier, isEmptyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; diff --git a/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts b/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts index 56e4d92fdc9..553be4fa37c 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspaces.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'vs/base/common/path'; @@ -23,7 +23,7 @@ flakySuite('Workspaces', () => { setup(async () => { testDir = getRandomTestPath(tmpDir, 'vsctests', 'workspacesmanagementmainservice'); - return pfs.Promises.mkdir(testDir, { recursive: true }); + return fs.promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts index 2677ffba87d..f2102c3b098 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index 0234ce57bb9..92f1ac9f589 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as os from 'os'; import { isUNC, toSlashes } from 'vs/base/common/extpath'; @@ -112,7 +112,7 @@ flakySuite('WorkspacesManagementMainService', () => { const fileService = new FileService(logService); service = new WorkspacesManagementMainService(environmentMainService, logService, new UserDataProfilesMainService(new StateService(SaveStrategy.DELAYED, environmentMainService, logService, fileService), new UriIdentityService(fileService), environmentMainService, fileService, logService), new TestBackupMainService(), new TestDialogMainService()); - return pfs.Promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); + return fs.promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); }); teardown(() => { diff --git a/src/vs/server/node/extensionHostConnection.ts b/src/vs/server/node/extensionHostConnection.ts index f259ea2cbaf..f345bd69a2d 100644 --- a/src/vs/server/node/extensionHostConnection.ts +++ b/src/vs/server/node/extensionHostConnection.ts @@ -5,23 +5,23 @@ import * as cp from 'child_process'; import * as net from 'net'; -import { getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; -import { FileAccess } from 'vs/base/common/network'; -import { join, delimiter } from 'vs/base/common/path'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; -import { createRandomIPCHandle, NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection'; -import { IExtHostReadyMessage, IExtHostSocketMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; +import { delimiter, join } from 'vs/base/common/path'; import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; import { removeDangerousEnvVariables } from 'vs/base/common/processes'; -import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { IPCExtHostConnection, writeExtHostConnection, SocketExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; +import { createRandomIPCHandle, NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection'; +import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; +import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; +import { getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; +import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; +import { IPCExtHostConnection, SocketExtHostConnection, writeExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; +import { IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, IExtHostSocketMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; export async function buildUserEnvironment(startParamsEnv: { [key: string]: string | null } = {}, withUserShellEnvironment: boolean, language: string, environmentService: IServerEnvironmentService, logService: ILogService, configurationService: IConfigurationService): Promise { const nlsConfig = await getNLSConfiguration(language, environmentService.userDataPath); @@ -43,7 +43,7 @@ export async function buildUserEnvironment(startParamsEnv: { [key: string]: stri ...{ VSCODE_AMD_ENTRYPOINT: 'vs/workbench/api/node/extensionHostProcess', VSCODE_HANDLES_UNCAUGHT_ERRORS: 'true', - VSCODE_NLS_CONFIG: JSON.stringify(nlsConfig, undefined, 0) + VSCODE_NLS_CONFIG: JSON.stringify(nlsConfig) }, ...startParamsEnv }; @@ -103,7 +103,7 @@ class ConnectionData { } } -export class ExtensionHostConnection { +export class ExtensionHostConnection extends Disposable { private _onClose = new Emitter(); readonly onClose: Event = this._onClose.event; @@ -124,6 +124,7 @@ export class ExtensionHostConnection { @IExtensionHostStatusService private readonly _extensionHostStatusService: IExtensionHostStatusService, @IConfigurationService private readonly _configurationService: IConfigurationService ) { + super(); this._canSendSocket = (!isWindows || !this._environmentService.args['socket-path']); this._disposed = false; this._remoteAddress = remoteAddress; @@ -133,6 +134,11 @@ export class ExtensionHostConnection { this._log(`New connection established.`); } + override dispose(): void { + this._cleanResources(); + super.dispose(); + } + private get _logPrefix(): string { return `[${this._remoteAddress}][${this._reconnectionToken.substr(0, 8)}][ExtensionHostConnection] `; } @@ -271,8 +277,8 @@ export class ExtensionHostConnection { this._extensionHostProcess.stderr!.setEncoding('utf8'); const onStdout = Event.fromNodeEventEmitter(this._extensionHostProcess.stdout!, 'data'); const onStderr = Event.fromNodeEventEmitter(this._extensionHostProcess.stderr!, 'data'); - onStdout((e) => this._log(`<${pid}> ${e}`)); - onStderr((e) => this._log(`<${pid}> ${e}`)); + this._register(onStdout((e) => this._log(`<${pid}> ${e}`))); + this._register(onStderr((e) => this._log(`<${pid}> ${e}`))); // Lifecycle this._extensionHostProcess.on('error', (err) => { diff --git a/src/vs/server/node/extensionsScannerService.ts b/src/vs/server/node/extensionsScannerService.ts index 5430ae19162..78dc3bce4f0 100644 --- a/src/vs/server/node/extensionsScannerService.ts +++ b/src/vs/server/node/extensionsScannerService.ts @@ -14,7 +14,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { getNLSConfiguration, InternalNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; +import { getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; export class ExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService { @@ -38,9 +38,9 @@ export class ExtensionsScannerService extends AbstractExtensionsScannerService i protected async getTranslations(language: string): Promise { const config = await getNLSConfiguration(language, this.nativeEnvironmentService.userDataPath); - if (InternalNLSConfiguration.is(config)) { + if (config.languagePack) { try { - const content = await this.fileService.readFile(URI.file(config._translationsConfigFile)); + const content = await this.fileService.readFile(URI.file(config.languagePack.translationsConfigFile)); return JSON.parse(content.value.toString()); } catch (err) { /* Ignore error */ } } diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 9eace08d06a..29aa95e5683 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -505,6 +505,7 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { this._extHostConnections[reconnectionToken] = con; this._allReconnectionTokens.add(reconnectionToken); con.onClose(() => { + con.dispose(); delete this._extHostConnections[reconnectionToken]; this._onDidCloseExtHostConnection(); }); diff --git a/src/vs/server/node/remoteExtensionsScanner.ts b/src/vs/server/node/remoteExtensionsScanner.ts index 54418aebfc2..7b58140ea1b 100644 --- a/src/vs/server/node/remoteExtensionsScanner.ts +++ b/src/vs/server/node/remoteExtensionsScanner.ts @@ -103,26 +103,6 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return extensions; } - async scanSingleExtension(extensionLocation: URI, isBuiltin: boolean, language?: string): Promise { - await this._whenBuiltinExtensionsReady; - - const extensionPath = extensionLocation.scheme === Schemas.file ? extensionLocation.fsPath : null; - - if (!extensionPath) { - return null; - } - - const extension = await this._scanSingleExtension(extensionPath, isBuiltin, language ?? platform.language); - - if (!extension) { - return null; - } - - this._massageWhenConditions([extension]); - - return extension; - } - private async _scanExtensions(profileLocation: URI, language: string, workspaceInstalledExtensionLocations: URI[] | undefined, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise { await this._ensureLanguagePackIsInstalled(language, languagePackId); @@ -133,7 +113,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS this._scanDevelopedExtensions(language, extensionDevelopmentPath) ]); - return dedupExtensions(builtinExtensions, [...installedExtensions, ...workspaceInstalledExtensions], developedExtensions, this._logService); + return dedupExtensions(builtinExtensions, installedExtensions, workspaceInstalledExtensions, developedExtensions, this._logService); } private async _scanDevelopedExtensions(language: string, extensionDevelopmentPaths?: string[]): Promise { @@ -168,13 +148,6 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return scannedExtensions.map(e => toExtensionDescription(e, false)); } - private async _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string): Promise { - const extensionLocation = URI.file(resolve(extensionPath)); - const type = isBuiltin ? ExtensionType.System : ExtensionType.User; - const scannedExtension = await this._extensionsScannerService.scanExistingExtension(extensionLocation, type, { language }); - return scannedExtension ? toExtensionDescription(scannedExtension, false) : null; - } - private async _ensureLanguagePackIsInstalled(language: string, languagePackId: string | undefined): Promise { if ( // No need to install language packs for the default language @@ -351,10 +324,6 @@ export class RemoteExtensionsScannerChannel implements IServerChannel { ); return extensions.map(extension => transformOutgoingURIs(extension, uriTransformer)); } - case 'scanSingleExtension': { - const extension = await this.service.scanSingleExtension(URI.revive(uriTransformer.transformIncoming(args[0])), args[1], args[2]); - return extension ? transformOutgoingURIs(extension, uriTransformer) : null; - } } throw new Error('Invalid call'); } diff --git a/src/vs/server/node/remoteLanguagePacks.ts b/src/vs/server/node/remoteLanguagePacks.ts index 682b6f2b088..2a1ea9f5699 100644 --- a/src/vs/server/node/remoteLanguagePacks.ts +++ b/src/vs/server/node/remoteLanguagePacks.ts @@ -3,46 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import { FileAccess } from 'vs/base/common/network'; -import * as path from 'vs/base/common/path'; - -import * as lp from 'vs/base/node/languagePacks'; +import { join } from 'vs/base/common/path'; +import type { INLSConfiguration } from 'vs/nls'; +import { resolveNLSConfiguration } from 'vs/base/node/nls'; +import { Promises } from 'vs/base/node/pfs'; import product from 'vs/platform/product/common/product'; -const metaData = path.join(FileAccess.asFileUri('').fsPath, 'nls.metadata.json'); -const _cache: Map> = new Map(); +const nlsMetadataPath = join(FileAccess.asFileUri('').fsPath); +const defaultMessagesFile = join(nlsMetadataPath, 'nls.messages.json'); +const nlsConfigurationCache = new Map>(); -function exists(file: string) { - return new Promise(c => fs.exists(file, c)); -} +export async function getNLSConfiguration(language: string, userDataPath: string): Promise { + if (!product.commit || !(await Promises.exists(defaultMessagesFile))) { + return { + userLocale: 'en', + osLocale: 'en', + resolvedLanguage: 'en', + defaultMessagesFile, -export function getNLSConfiguration(language: string, userDataPath: string): Promise { - return exists(metaData).then((fileExists) => { - if (!fileExists || !product.commit) { - // console.log(`==> MetaData or commit unknown. Using default language.`); - // The OS Locale on the remote side really doesn't matter, so we return the default locale - return Promise.resolve({ locale: 'en', osLocale: 'en', availableLanguages: {} }); - } - const key = `${language}||${userDataPath}`; - let result = _cache.get(key); - if (!result) { - // The OS Locale on the remote side really doesn't matter, so we pass in the same language - result = lp.getNLSConfiguration(product.commit, userDataPath, metaData, language, language).then(value => { - if (InternalNLSConfiguration.is(value)) { - value._languagePackSupport = true; - } - return value; - }); - _cache.set(key, result); - } - return result; - }); -} - -export namespace InternalNLSConfiguration { - export function is(value: lp.NLSConfiguration): value is lp.InternalNLSConfiguration { - const candidate: lp.InternalNLSConfiguration = value as lp.InternalNLSConfiguration; - return candidate && typeof candidate._languagePackId === 'string'; + // NLS: below 2 are a relic from old times only used by vscode-nls and deprecated + locale: 'en', + availableLanguages: {} + }; } + + const cacheKey = `${language}||${userDataPath}`; + let result = nlsConfigurationCache.get(cacheKey); + if (!result) { + result = resolveNLSConfiguration({ userLocale: language, osLocale: language, commit: product.commit, userDataPath, nlsMetadataPath }); + nlsConfigurationCache.set(cacheKey, result); + } + + return result; } diff --git a/src/vs/server/node/serverConnectionToken.ts b/src/vs/server/node/serverConnectionToken.ts index f976d0e379a..6a6bdcdb56a 100644 --- a/src/vs/server/node/serverConnectionToken.ts +++ b/src/vs/server/node/serverConnectionToken.ts @@ -100,7 +100,7 @@ export async function determineServerConnectionToken(args: ServerParsedArgs): Pr // First try to find a connection token try { - const fileContents = await Promises.readFile(storageLocation); + const fileContents = await fs.promises.readFile(storageLocation); const connectionToken = fileContents.toString().replace(/\r?\n$/, ''); if (connectionTokenRegex.test(connectionToken)) { return connectionToken; diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index 6dc550b6cf0..a8ac6043a51 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createReadStream } from 'fs'; -import { Promises } from 'vs/base/node/pfs'; +import { createReadStream, promises } from 'fs'; import * as path from 'path'; import * as http from 'http'; import * as url from 'url'; @@ -55,7 +54,7 @@ export const enum CacheControl { */ export async function serveFile(filePath: string, cacheControl: CacheControl, logService: ILogService, req: http.IncomingMessage, res: http.ServerResponse, responseHeaders: Record): Promise { try { - const stat = await Promises.stat(filePath); // throws an error if file doesn't exist + const stat = await promises.stat(filePath); // throws an error if file doesn't exist if (cacheControl === CacheControl.ETAG) { // Check if file modified since @@ -320,7 +319,7 @@ export class WebClientServer { if (!this._environmentService.isBuilt) { try { - const productOverrides = JSON.parse((await Promises.readFile(join(APP_ROOT, 'product.overrides.json'))).toString()); + const productOverrides = JSON.parse((await promises.readFile(join(APP_ROOT, 'product.overrides.json'))).toString()); Object.assign(productConfiguration, productOverrides); } catch (err) {/* Ignore Error */ } } @@ -338,18 +337,29 @@ export class WebClientServer { callbackRoute: this._callbackRoute }; - const nlsBaseUrl = this._productService.extensionsGallery?.nlsBaseUrl; + const cookies = cookie.parse(req.headers.cookie || ''); + const locale = cookies['vscode.nls.locale'] || req.headers['accept-language']?.split(',')[0]?.toLowerCase() || 'en'; + let WORKBENCH_NLS_BASE_URL: string | undefined; + let WORKBENCH_NLS_URL: string; + if (!locale.startsWith('en') && this._productService.nlsCoreBaseUrl) { + WORKBENCH_NLS_BASE_URL = this._productService.nlsCoreBaseUrl; + WORKBENCH_NLS_URL = `${WORKBENCH_NLS_BASE_URL}${this._productService.commit}/${this._productService.version}/${locale}/nls.messages.js`; + } else { + WORKBENCH_NLS_URL = ''; // fallback will apply + } + const values: { [key: string]: string } = { WORKBENCH_WEB_CONFIGURATION: asJSON(workbenchWebConfiguration), WORKBENCH_AUTH_SESSION: authSessionInfo ? asJSON(authSessionInfo) : '', WORKBENCH_WEB_BASE_URL: this._staticRoute, - WORKBENCH_NLS_BASE_URL: nlsBaseUrl ? `${nlsBaseUrl}${!nlsBaseUrl.endsWith('/') ? '/' : ''}${this._productService.commit}/${this._productService.version}/` : '', + WORKBENCH_NLS_URL, + WORKBENCH_NLS_FALLBACK_URL: `${this._staticRoute}/out/nls.messages.js` }; if (useTestResolver) { const bundledExtensions: { extensionPath: string; packageJSON: IExtensionManifest }[] = []; for (const extensionPath of ['vscode-test-resolver', 'github-authentication']) { - const packageJSON = JSON.parse((await Promises.readFile(FileAccess.asFileUri(`${builtinExtensionsPath}/${extensionPath}/package.json`).fsPath)).toString()); + const packageJSON = JSON.parse((await promises.readFile(FileAccess.asFileUri(`${builtinExtensionsPath}/${extensionPath}/package.json`).fsPath)).toString()); bundledExtensions.push({ extensionPath, packageJSON }); } values['WORKBENCH_BUILTIN_EXTENSIONS'] = asJSON(bundledExtensions); @@ -357,20 +367,20 @@ export class WebClientServer { let data; try { - const workbenchTemplate = (await Promises.readFile(filePath)).toString(); + const workbenchTemplate = (await promises.readFile(filePath)).toString(); data = workbenchTemplate.replace(/\{\{([^}]+)\}\}/g, (_, key) => values[key] ?? 'undefined'); } catch (e) { res.writeHead(404, { 'Content-Type': 'text/plain' }); return void res.end('Not found'); } - const webWorkerExtensionHostIframeScriptSHA = 'sha256-75NYUUvf+5++1WbfCZOV3PSWxBhONpaxwx+mkOFRv/Y='; + const webWorkerExtensionHostIframeScriptSHA = 'sha256-V28GQnL3aYxbwgpV3yW1oJ+VKKe/PBSzWntNyH8zVXA='; const cspDirectives = [ 'default-src \'self\';', 'img-src \'self\' https: data: blob:;', 'media-src \'self\';', - `script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} '${webWorkerExtensionHostIframeScriptSHA}' ${useTestResolver ? '' : `http://${remoteAuthority}`};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html + `script-src 'self' 'unsafe-eval' ${WORKBENCH_NLS_BASE_URL ?? ''} ${this._getScriptCspHashes(data).join(' ')} '${webWorkerExtensionHostIframeScriptSHA}' ${useTestResolver ? '' : `http://${remoteAuthority}`};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html 'child-src \'self\';', `frame-src 'self' https://*.vscode-cdn.net data:;`, 'worker-src \'self\' data: blob:;', @@ -426,7 +436,7 @@ export class WebClientServer { */ private async _handleCallback(res: http.ServerResponse): Promise { const filePath = FileAccess.asFileUri('vs/code/browser/workbench/callback.html').fsPath; - const data = (await Promises.readFile(filePath)).toString(); + const data = (await promises.readFile(filePath)).toString(); const cspDirectives = [ 'default-src \'self\';', 'img-src \'self\' https: data: blob:;', diff --git a/src/vs/server/test/node/serverConnectionToken.test.ts b/src/vs/server/test/node/serverConnectionToken.test.ts index b624dd6d676..b04dab4d308 100644 --- a/src/vs/server/test/node/serverConnectionToken.test.ts +++ b/src/vs/server/test/node/serverConnectionToken.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 9fdc4b30306..1d563ea1dce 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -20,6 +20,7 @@ import './mainThreadBulkEdits'; import './mainThreadLanguageModels'; import './mainThreadChatAgents2'; import './mainThreadChatVariables'; +import './mainThreadLanguageModelTools'; import './mainThreadEmbeddings'; import './mainThreadCodeInsets'; import './mainThreadCLICommands'; diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 3719825841a..5780ec8797e 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -6,7 +6,7 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, INTERNAL_AUTH_PROVIDER_PREFIX as INTERNAL_MODEL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, INTERNAL_AUTH_PROVIDER_PREFIX as INTERNAL_MODEL_AUTH_PROVIDER_PREFIX, AuthenticationSessionAccount, IAuthenticationProviderSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; @@ -31,6 +31,7 @@ interface AuthenticationGetSessionOptions { createIfNone?: boolean; forceNewSession?: boolean | AuthenticationForceNewSessionOptions; silent?: boolean; + account?: AuthenticationSessionAccount; } export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -49,8 +50,8 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut this.onDidChangeSessions = onDidChangeSessionsEmitter.event; } - async getSessions(scopes?: string[]) { - return this._proxy.$getSessions(this.id, scopes); + async getSessions(scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions) { + return this._proxy.$getSessions(this.id, scopes, options); } createSession(scopes: string[], options: IAuthenticationCreateSessionOptions): Promise { @@ -159,7 +160,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { - const sessions = await this.authenticationService.getSessions(providerId, scopes, true); + const sessions = await this.authenticationService.getSessions(providerId, scopes, options.account, true); const provider = this.authenticationService.getProvider(providerId); // Error cases @@ -213,18 +214,16 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu let session; if (sessions?.length && !options.forceNewSession) { - session = provider.supportsMultipleAccounts + session = provider.supportsMultipleAccounts && !options.account ? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopes, sessions) : sessions[0]; } else { - let sessionToRecreate: AuthenticationSession | undefined; - if (typeof options.forceNewSession === 'object' && options.forceNewSession.sessionToRecreate) { - sessionToRecreate = options.forceNewSession.sessionToRecreate as AuthenticationSession; - } else { + let account: AuthenticationSessionAccount | undefined = options.account; + if (!account) { const sessionIdToRecreate = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); - sessionToRecreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate) : undefined; + account = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate)?.account : undefined; } - session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, sessionToRecreate }); + session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, account }); } this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); @@ -261,16 +260,17 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu return session; } - async $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise { - const sessions = await this.authenticationService.getSessions(providerId, [...scopes], true); - const accessibleSessions = sessions.filter(s => this.authenticationAccessService.isAccessAllowed(providerId, s.account.label, extensionId)); - if (accessibleSessions.length) { - this.sendProviderUsageTelemetry(extensionId, providerId); - for (const session of accessibleSessions) { - this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); + async $getAccounts(providerId: string): Promise> { + const sessions = await this.authenticationService.getSessions(providerId); + const accounts = new Array(); + const seenAccounts = new Set(); + for (const session of sessions) { + if (!seenAccounts.has(session.account.label)) { + seenAccounts.add(session.account.label); + accounts.push(session.account); } } - return accessibleSessions; + return accounts; } private sendProviderUsageTelemetry(extensionId: string, providerId: string): void { diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 0d63847dae5..35126b7998d 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -199,7 +199,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void { const data = this._agents.get(handle); if (!data) { - throw new Error(`No agent with handle ${handle} registered`); + this._logService.error(`MainThreadChatAgents2#$updateAgent: No agent with handle ${handle} registered`); + return; } data.hasFollowups = metadataUpdate.hasFollowups; this._chatAgentService.updateAgent(data.id, revive(metadataUpdate)); diff --git a/src/vs/workbench/api/browser/mainThreadChatVariables.ts b/src/vs/workbench/api/browser/mainThreadChatVariables.ts index bf7103206a0..9e08e5d1423 100644 --- a/src/vs/workbench/api/browser/mainThreadChatVariables.ts +++ b/src/vs/workbench/api/browser/mainThreadChatVariables.ts @@ -5,10 +5,7 @@ import { DisposableMap } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; -import { URI } from 'vs/base/common/uri'; -import { Location } from 'vs/editor/common/languages'; import { ExtHostChatVariablesShape, ExtHostContext, IChatVariableResolverProgressDto, MainContext, MainThreadChatVariablesShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress, IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -50,8 +47,4 @@ export class MainThreadChatVariables implements MainThreadChatVariablesShape { $unregisterVariable(handle: number): void { this._variables.deleteAndDispose(handle); } - - $attachContext(name: string, value: string | URI | Location | unknown, location: ChatAgentLocation.Panel): void { - this._chatVariablesService.attachContext(name, revive(value), location); - } } diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index d5e11a86dc1..a0f00ce4416 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -27,6 +27,9 @@ import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { Schemas } from 'vs/base/common/network'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { MarshalledCommentThread } from 'vs/workbench/common/comments'; +import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; export class MainThreadCommentThread implements languages.CommentThread { private _input?: languages.CommentInput; @@ -109,8 +112,10 @@ export class MainThreadCommentThread implements languages.CommentThread { } set collapsibleState(newState: languages.CommentThreadCollapsibleState | undefined) { - this._collapsibleState = newState; - this._onDidChangeCollapsibleState.fire(this._collapsibleState); + if (newState !== this._collapsibleState) { + this._collapsibleState = newState; + this._onDidChangeCollapsibleState.fire(this._collapsibleState); + } } private _initialCollapsibleState: languages.CommentThreadCollapsibleState | undefined; @@ -179,6 +184,7 @@ export class MainThreadCommentThread implements languages.CommentThread { public threadId: string, public resource: string, private _range: T | undefined, + comments: languages.Comment[] | undefined, private _canReply: boolean, private _isTemplate: boolean, public editorId?: string @@ -186,6 +192,8 @@ export class MainThreadCommentThread implements languages.CommentThread { this._isDisposed = false; if (_isTemplate) { this.comments = []; + } else if (comments) { + this._comments = comments; } } @@ -292,6 +300,7 @@ export class MainThreadCommentController implements ICommentController { threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, + comments: languages.Comment[], isTemplate: boolean, editorId?: string ): languages.CommentThread { @@ -302,6 +311,7 @@ export class MainThreadCommentController implements ICommentController { threadId, URI.revive(resource).toString(), range, + comments, true, isTemplate, editorId @@ -518,7 +528,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments extHostContext: IExtHostContext, @ICommentService private readonly _commentService: ICommentService, @IViewsService private readonly _viewsService: IViewsService, - @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService + @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, + @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, + @IEditorService private readonly _editorService: IEditorService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); @@ -582,6 +594,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, + comments: languages.Comment[], extensionId: ExtensionIdentifier, isTemplate: boolean, editorId?: string @@ -592,7 +605,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments return undefined; } - return provider.createCommentThread(extensionId.value, commentThreadHandle, threadId, resource, range, isTemplate, editorId); + return provider.createCommentThread(extensionId.value, commentThreadHandle, threadId, resource, range, comments, isTemplate, editorId); } $updateCommentThread(handle: number, @@ -629,6 +642,21 @@ export class MainThreadComments extends Disposable implements MainThreadComments provider.updateCommentingRanges(resourceHints); } + async $revealCommentThread(handle: number, commentThreadHandle: number, options: languages.CommentThreadRevealOptions): Promise { + const provider = this._commentControllers.get(handle); + + if (!provider) { + return Promise.resolve(); + } + + const thread = provider.getAllComments().find(thread => thread.commentThreadHandle === commentThreadHandle); + if (!thread || !thread.isDocumentCommentThread()) { + return Promise.resolve(); + } + + revealCommentThread(this._commentService, this._editorService, this._uriIdentityService, thread, undefined, options.focusReply, undefined, options.preserveFocus); + } + private registerView(commentsViewAlreadyRegistered: boolean) { if (!commentsViewAlreadyRegistered) { const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index f58ba4c47fb..36b5a4df0ae 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -329,6 +329,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb compact: options.compact, compoundRoot: parentSession?.compoundRoot, saveBeforeRestart: saveBeforeStart, + testRun: options.testRun, suppressDebugStatusbar: options.suppressDebugStatusbar, suppressDebugToolbar: options.suppressDebugToolbar, diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index 48888b0ddcb..9bf27279db0 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -207,11 +207,11 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { if (editor instanceof MultiDiffEditorInput) { const diffEditors: TextDiffInputDto[] = []; for (const resource of (editor?.resources.get() ?? [])) { - if (resource.original && resource.modified) { + if (resource.originalUri && resource.modifiedUri) { diffEditors.push({ kind: TabInputKind.TextDiffInput, - original: resource.original, - modified: resource.modified + original: resource.originalUri, + modified: resource.modifiedUri }); } } diff --git a/src/vs/workbench/api/browser/mainThreadErrors.ts b/src/vs/workbench/api/browser/mainThreadErrors.ts index 2d05a6a0e44..e1591179944 100644 --- a/src/vs/workbench/api/browser/mainThreadErrors.ts +++ b/src/vs/workbench/api/browser/mainThreadErrors.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SerializedError, onUnexpectedError, ErrorNoTelemetry } from 'vs/base/common/errors'; +import { SerializedError, onUnexpectedError, transformErrorFromSerialization } from 'vs/base/common/errors'; import { extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { MainContext, MainThreadErrorsShape } from 'vs/workbench/api/common/extHost.protocol'; @@ -16,11 +16,7 @@ export class MainThreadErrors implements MainThreadErrorsShape { $onUnexpectedError(err: any | SerializedError): void { if (err && err.$isError) { - const { name, message, stack } = err; - err = err.noTelemetry ? new ErrorNoTelemetry() : new Error(); - err.message = message; - err.name = name; - err.stack = stack; + err = transformErrorFromSerialization(err); } onUnexpectedError(err); } diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 6732d38f5e6..7c4db0a5def 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -6,7 +6,7 @@ import { Action } from 'vs/base/common/actions'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { SerializedError } from 'vs/base/common/errors'; +import { SerializedError, transformErrorFromSerialization } from 'vs/base/common/errors'; import { FileAccess } from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -73,19 +73,13 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha this._internalExtensionService._onDidActivateExtension(extensionId, codeLoadingTime, activateCallTime, activateResolvedTime, activationReason); } $onExtensionRuntimeError(extensionId: ExtensionIdentifier, data: SerializedError): void { - const error = new Error(); - error.name = data.name; - error.message = data.message; - error.stack = data.stack; + const error = transformErrorFromSerialization(data); this._internalExtensionService._onExtensionRuntimeError(extensionId, error); console.error(`[${extensionId.value}]${error.message}`); console.error(error.stack); } async $onExtensionActivationError(extensionId: ExtensionIdentifier, data: SerializedError, missingExtensionDependency: MissingExtensionDependency | null): Promise { - const error = new Error(); - error.name = data.name; - error.message = data.message; - error.stack = data.stack; + const error = transformErrorFromSerialization(data); this._internalExtensionService._onDidActivateExtensionError(extensionId, error); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index a75f2154c9d..4aa61aeab45 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -67,7 +67,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } this._proxy.$setWordDefinitions(wordDefinitionDtos); }; - this._languageConfigurationService.onDidChange((e) => { + this._register(this._languageConfigurationService.onDidChange((e) => { if (!e.languageId) { updateAllWordDefinitions(); } else { @@ -78,7 +78,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread regexFlags: wordDefinition.flags }]); } - }); + })); updateAllWordDefinitions(); } } @@ -612,6 +612,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { return this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token); }, + provideInlineEdits: async (model: ITextModel, range: EditorRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { + return this._proxy.$provideInlineEdits(handle, model.uri, range, context, token); + }, handleItemDidShow: async (completions: IdentifiableInlineCompletions, item: IdentifiableInlineCompletion, updatedInsertText: string): Promise => { if (supportsHandleEvents) { await this._proxy.$handleInlineCompletionDidShow(handle, completions.pid, item.idx, updatedInsertText); @@ -1124,7 +1127,7 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentDropEdit } } - async provideDocumentDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + async provideDocumentDropEdits(model: ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { const request = this.dataTransfers.add(dataTransfer); try { const dataTransferDto = await typeConvert.DataTransfer.from(dataTransfer); @@ -1137,14 +1140,19 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentDropEdit return; } - return edits.map(edit => { - return { - ...edit, - yieldTo: edit.yieldTo?.map(x => ({ kind: new HierarchicalKind(x) })), - kind: edit.kind ? new HierarchicalKind(edit.kind) : undefined, - additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), - }; - }); + return { + edits: edits.map(edit => { + return { + ...edit, + yieldTo: edit.yieldTo?.map(x => ({ kind: new HierarchicalKind(x) })), + kind: edit.kind ? new HierarchicalKind(edit.kind) : undefined, + additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit, this._uriIdentService, dataId => this.resolveDocumentOnDropFileData(request.id, dataId)), + }; + }), + dispose: () => { + this._proxy.$releaseDocumentOnDropEdits(this._handle, request.id); + }, + }; } finally { request.dispose(); } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts new file mode 100644 index 00000000000..fbda6ced5a3 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; +import { ExtHostLanguageModelToolsShape, ExtHostContext, MainContext, MainThreadLanguageModelToolsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IToolData, ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; + +@extHostNamedCustomer(MainContext.MainThreadLanguageModelTools) +export class MainThreadLanguageModelTools extends Disposable implements MainThreadLanguageModelToolsShape { + + private readonly _proxy: ExtHostLanguageModelToolsShape; + private readonly _tools = this._register(new DisposableMap()); + + constructor( + extHostContext: IExtHostContext, + @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageModelTools); + + this._register(this._languageModelToolsService.onDidChangeTools(e => this._proxy.$acceptToolDelta(e))); + } + + async $getTools(): Promise { + return Array.from(this._languageModelToolsService.getTools()); + } + + $invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + return this._languageModelToolsService.invokeTool(name, parameters, token); + } + + $registerTool(name: string): void { + const disposable = this._languageModelToolsService.registerToolImplementation( + name, + { + invoke: async (parameters, token) => { + return await this._proxy.$invokeTool(name, parameters, token); + }, + }); + this._tools.set(name, disposable); + } + + $unregisterTool(name: string): void { + this._tools.deleteAndDispose(name); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 9b14928a514..adea665487c 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -3,18 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { AsyncIterableSource, DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { SerializedError, transformErrorForSerialization, transformErrorFromSerialization } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { ExtHostLanguageModelsShape, ExtHostContext, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ILanguageModelStatsService } from 'vs/workbench/contrib/chat/common/languageModelStats'; -import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage, ILanguageModelChatSelector } from 'vs/workbench/contrib/chat/common/languageModels'; +import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage, ILanguageModelChatSelector, ILanguageModelChatResponse } from 'vs/workbench/contrib/chat/common/languageModels'; import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; -import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationProviderCreateSessionOptions, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; +import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -24,7 +25,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { private readonly _proxy: ExtHostLanguageModelsShape; private readonly _store = new DisposableStore(); private readonly _providerRegistrations = new DisposableMap(); - private readonly _pendingProgress = new Map>(); + private readonly _pendingProgress = new Map; stream: AsyncIterableSource }>(); constructor( extHostContext: IExtHostContext, @@ -49,14 +50,23 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { const dipsosables = new DisposableStore(); dipsosables.add(this._chatProviderService.registerLanguageModelChat(identifier, { metadata, - provideChatResponse: async (messages, from, options, progress, token) => { + sendChatRequest: async (messages, from, options, token) => { const requestId = (Math.random() * 1e6) | 0; - this._pendingProgress.set(requestId, progress); + const defer = new DeferredPromise(); + const stream = new AsyncIterableSource(); + try { - await this._proxy.$provideLanguageModelResponse(handle, requestId, from, messages, options, token); - } finally { + this._pendingProgress.set(requestId, { defer, stream }); + await this._proxy.$startChatRequest(handle, requestId, from, messages, options, token); + } catch (err) { this._pendingProgress.delete(requestId); + throw err; } + + return { + result: defer.p, + stream: stream.asyncIterable + } satisfies ILanguageModelChatResponse; }, provideTokenCount: (str, token) => { return this._proxy.$provideTokenLength(handle, str, token); @@ -68,8 +78,28 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { this._providerRegistrations.set(handle, dipsosables); } - async $handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise { - this._pendingProgress.get(requestId)?.report(chunk); + async $reportResponsePart(requestId: number, chunk: IChatResponseFragment): Promise { + const data = this._pendingProgress.get(requestId); + this._logService.trace('[LM] report response PART', Boolean(data), requestId, chunk); + if (data) { + data.stream.emitOne(chunk); + } + } + + async $reportResponseDone(requestId: number, err: SerializedError | undefined): Promise { + const data = this._pendingProgress.get(requestId); + this._logService.trace('[LM] report response DONE', Boolean(data), requestId, err); + if (data) { + this._pendingProgress.delete(requestId); + if (err) { + const error = transformErrorFromSerialization(err); + data.stream.reject(error); + data.defer.error(error); + } else { + data.stream.resolve(); + data.defer.complete(undefined); + } + } } $unregisterProvider(handle: number): void { @@ -84,21 +114,36 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { this._languageModelStatsService.update(identifier, extensionId, participant, tokenCount); } - async $fetchResponse(extension: ExtensionIdentifier, providerId: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise { - this._logService.debug('[CHAT] extension request STARTED', extension.value, requestId); + async $tryStartChatRequest(extension: ExtensionIdentifier, providerId: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise { + this._logService.trace('[CHAT] request STARTED', extension.value, requestId); - const task = this._chatProviderService.makeLanguageModelChatRequest(providerId, extension, messages, options, new Progress(value => { - this._proxy.$handleResponseFragment(requestId, value); - }), token); + const response = await this._chatProviderService.sendChatRequest(providerId, extension, messages, options, token); - task.catch(err => { - this._logService.error('[CHAT] extension request ERRORED', err, extension.value, requestId); - throw err; - }).finally(() => { + // !!! IMPORTANT !!! + // This method must return before the response is done (has streamed all parts) + // and because of that we consume the stream without awaiting + // !!! IMPORTANT !!! + const streaming = (async () => { + try { + for await (const part of response.stream) { + this._logService.trace('[CHAT] request PART', extension.value, requestId, part); + await this._proxy.$acceptResponsePart(requestId, part); + } + this._logService.trace('[CHAT] request DONE', extension.value, requestId); + } catch (err) { + this._logService.error('[CHAT] extension request ERRORED in STREAM', err, extension.value, requestId); + this._proxy.$acceptResponseDone(requestId, transformErrorForSerialization(err)); + } + })(); + + // When the response is done (signaled via its result) we tell the EH + Promise.allSettled([response.result, streaming]).then(() => { this._logService.debug('[CHAT] extension request DONE', extension.value, requestId); + this._proxy.$acceptResponseDone(requestId, undefined); + }, err => { + this._logService.error('[CHAT] extension request ERRORED', err, extension.value, requestId); + this._proxy.$acceptResponseDone(requestId, transformErrorForSerialization(err)); }); - - return task; } @@ -161,9 +206,9 @@ class LanguageModelAccessAuthProvider implements IAuthenticationProvider { if (this._session) { return [this._session]; } - return [await this.createSession(scopes || [], {})]; + return [await this.createSession(scopes || [])]; } - async createSession(scopes: string[], options: IAuthenticationProviderCreateSessionOptions): Promise { + async createSession(scopes: string[]): Promise { this._session = this._createFakeSession(scopes); this._onDidChangeSessions.fire({ added: [this._session], changed: [], removed: [] }); return this._session; diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index c036901f907..b50fee3527c 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -104,7 +104,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { }; } - const thisPriorityInfo = coalesce([{ isFromSettings: false, filenamePatterns: includes }, ...allPriorityInfo.get(viewType) ?? []]); + const thisPriorityInfo = coalesce([{ isFromSettings: false, filenamePatterns: includes }, ...allPriorityInfo.get(viewType) ?? []]); const otherEditorsPriorityInfo = Array.from(allPriorityInfo.keys()) .flatMap(key => { if (key !== viewType) { diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 348cd234e76..af9d3f401cf 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Barrier } from 'vs/base/common/async'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; +import { observableValue } from 'vs/base/common/observable'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; -import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol'; import { Command } from 'vs/editor/common/languages'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -39,6 +41,13 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da } } +function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem { + const icon = getIconFromIconDto(historyItemDto.icon); + const labels = historyItemDto.labels?.map(l => ({ title: l.title, icon: getIconFromIconDto(l.icon) })); + + return { ...historyItemDto, icon, labels }; +} + class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider { constructor( textModelService: ITextModelService, @@ -162,6 +171,9 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { this._onDidChangeCurrentHistoryItemGroup.fire(); } + private readonly _currentHistoryItemGroupObs = observableValue(this, undefined); + get currentHistoryItemGroupObs() { return this._currentHistoryItemGroupObs; } + constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } async resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> { @@ -170,12 +182,17 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { async provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise { const historyItems = await this.proxy.$provideHistoryItems(this.handle, historyItemGroupId, options, CancellationToken.None); - return historyItems?.map(historyItem => ({ ...historyItem, icon: getIconFromIconDto(historyItem.icon) })); + return historyItems?.map(historyItem => toISCMHistoryItem(historyItem)); + } + + async provideHistoryItems2(options: ISCMHistoryOptions): Promise { + const historyItems = await this.proxy.$provideHistoryItems2(this.handle, options, CancellationToken.None); + return historyItems?.map(historyItem => toISCMHistoryItem(historyItem)); } async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { const historyItem = await this.proxy.$provideHistoryItemSummary(this.handle, historyItemId, historyItemParentId, CancellationToken.None); - return historyItem ? { ...historyItem, icon: getIconFromIconDto(historyItem.icon) } : undefined; + return historyItem ? toISCMHistoryItem(historyItem) : undefined; } async provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise { @@ -188,6 +205,9 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { })); } + $onDidChangeCurrentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined): void { + this._currentHistoryItemGroupObs.set(historyItemGroup, undefined); + } } class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { @@ -224,21 +244,21 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { get inputBoxTextModel(): ITextModel { return this._inputBoxTextModel; } get contextValue(): string { return this._providerId; } - get commitTemplate(): string { return this.features.commitTemplate || ''; } get historyProvider(): ISCMHistoryProvider | undefined { return this._historyProvider; } get acceptInputCommand(): Command | undefined { return this.features.acceptInputCommand; } get actionButton(): ISCMActionButtonDescriptor | undefined { return this.features.actionButton ?? undefined; } - get statusBarCommands(): Command[] | undefined { return this.features.statusBarCommands; } - get count(): number | undefined { return this.features.count; } + + private readonly _count = observableValue(this, undefined); + get count() { return this._count; } + + private readonly _statusBarCommands = observableValue(this, undefined); + get statusBarCommands() { return this._statusBarCommands; } private readonly _name: string | undefined; get name(): string { return this._name ?? this._label; } - private readonly _onDidChangeCommitTemplate = new Emitter(); - readonly onDidChangeCommitTemplate: Event = this._onDidChangeCommitTemplate.event; - - private readonly _onDidChangeStatusBarCommands = new Emitter(); - get onDidChangeStatusBarCommands(): Event { return this._onDidChangeStatusBarCommands.event; } + private readonly _commitTemplate = observableValue(this, ''); + get commitTemplate() { return this._commitTemplate; } private readonly _onDidChangeHistoryProvider = new Emitter(); readonly onDidChangeHistoryProvider: Event = this._onDidChangeHistoryProvider.event; @@ -250,6 +270,8 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { public readonly isSCM: boolean = true; private _historyProvider: ISCMHistoryProvider | undefined; + private readonly _historyProviderObs = observableValue(this, undefined); + get historyProviderObs() { return this._historyProviderObs; } constructor( private readonly proxy: ExtHostSCMShape, @@ -277,11 +299,15 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { this._onDidChange.fire(); if (typeof features.commitTemplate !== 'undefined') { - this._onDidChangeCommitTemplate.fire(this.commitTemplate); + this._commitTemplate.set(features.commitTemplate, undefined); + } + + if (typeof features.count !== 'undefined') { + this._count.set(features.count, undefined); } if (typeof features.statusBarCommands !== 'undefined') { - this._onDidChangeStatusBarCommands.fire(this.statusBarCommands!); + this._statusBarCommands.set(features.statusBarCommands, undefined); } if (features.hasQuickDiffProvider && !this._quickDiff) { @@ -297,9 +323,14 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { } if (features.hasHistoryProvider && !this._historyProvider) { - this._historyProvider = new MainThreadSCMHistoryProvider(this.proxy, this.handle); + const historyProvider = new MainThreadSCMHistoryProvider(this.proxy, this.handle); + this._historyProviderObs.set(historyProvider, undefined); + + this._historyProvider = historyProvider; this._onDidChangeHistoryProvider.fire(); } else if (features.hasHistoryProvider === false && this._historyProvider) { + this._historyProviderObs.set(undefined, undefined); + this._historyProvider = undefined; this._onDidChangeHistoryProvider.fire(); } @@ -423,6 +454,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { } this._historyProvider.currentHistoryItemGroup = currentHistoryItemGroup ?? undefined; + this._historyProviderObs.get()?.$onDidChangeCurrentHistoryItemGroup(currentHistoryItemGroup); } toJSON(): any { @@ -442,6 +474,7 @@ export class MainThreadSCM implements MainThreadSCMShape { private readonly _proxy: ExtHostSCMShape; private _repositories = new Map(); + private _repositoryBarriers = new Map(); private _repositoryDisposables = new Map(); private readonly _disposables = new DisposableStore(); @@ -472,9 +505,9 @@ export class MainThreadSCM implements MainThreadSCMShape { } async $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): Promise { - // Eagerly create the text model for the input box - const inputBoxTextModelRef = await this.textModelService.createModelReference(URI.revive(inputBoxDocumentUri)); + this._repositoryBarriers.set(handle, new Barrier()); + const inputBoxTextModelRef = await this.textModelService.createModelReference(URI.revive(inputBoxDocumentUri)); const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, inputBoxTextModelRef.object.textEditorModel, this.quickDiffService, this._uriIdentService, this.workspaceContextService); const repository = this.scmService.registerSCMProvider(provider); this._repositories.set(handle, repository); @@ -484,6 +517,7 @@ export class MainThreadSCM implements MainThreadSCMShape { Event.filter(this.scmViewService.onDidFocusRepository, r => r === repository)(_ => this._proxy.$setSelectedSourceControl(handle)), repository.input.onDidChange(({ value }) => this._proxy.$onInputBoxValueChange(handle, value)) ); + this._repositoryDisposables.set(handle, disposable); if (this.scmViewService.focusedRepository === repository) { setTimeout(() => this._proxy.$setSelectedSourceControl(handle), 0); @@ -493,10 +527,11 @@ export class MainThreadSCM implements MainThreadSCMShape { setTimeout(() => this._proxy.$onInputBoxValueChange(handle, repository.input.value), 0); } - this._repositoryDisposables.set(handle, disposable); + this._repositoryBarriers.get(handle)?.open(); } - $updateSourceControl(handle: number, features: SCMProviderFeatures): void { + async $updateSourceControl(handle: number, features: SCMProviderFeatures): Promise { + await this._repositoryBarriers.get(handle)?.wait(); const repository = this._repositories.get(handle); if (!repository) { @@ -507,7 +542,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$updateSourceControl(features); } - $unregisterSourceControl(handle: number): void { + async $unregisterSourceControl(handle: number): Promise { + await this._repositoryBarriers.get(handle)?.wait(); const repository = this._repositories.get(handle); if (!repository) { @@ -521,7 +557,8 @@ export class MainThreadSCM implements MainThreadSCMShape { this._repositories.delete(handle); } - $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): void { + async $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -533,7 +570,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$spliceGroupResourceStates(splices); } - $updateGroup(sourceControlHandle: number, groupHandle: number, features: SCMGroupFeatures): void { + async $updateGroup(sourceControlHandle: number, groupHandle: number, features: SCMGroupFeatures): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -544,7 +582,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$updateGroup(groupHandle, features); } - $updateGroupLabel(sourceControlHandle: number, groupHandle: number, label: string): void { + async $updateGroupLabel(sourceControlHandle: number, groupHandle: number, label: string): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -555,7 +594,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$updateGroupLabel(groupHandle, label); } - $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): void { + async $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -566,7 +606,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$spliceGroupResourceStates(splices); } - $unregisterGroup(sourceControlHandle: number, handle: number): void { + async $unregisterGroup(sourceControlHandle: number, handle: number): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -577,7 +618,8 @@ export class MainThreadSCM implements MainThreadSCMShape { provider.$unregisterGroup(handle); } - $setInputBoxValue(sourceControlHandle: number, value: string): void { + async $setInputBoxValue(sourceControlHandle: number, value: string): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -587,7 +629,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.setValue(value, false); } - $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void { + async $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -597,7 +640,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.placeholder = placeholder; } - $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): void { + async $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -607,7 +651,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.enabled = enabled; } - $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void { + async $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -617,7 +662,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.visible = visible; } - $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType) { + async $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { return; @@ -626,7 +672,8 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.showValidationMessage(message, type); } - $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void { + async $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { @@ -643,7 +690,8 @@ export class MainThreadSCM implements MainThreadSCMShape { } } - $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void { + async $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); if (!repository) { diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index a3641b6687a..1b2a115fca2 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -150,7 +150,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh let value = task.coverage.read(undefined); if (!value) { value = new TestCoverage(run, taskId, this.uriIdentityService, { - getCoverageDetails: (id, token) => this.proxy.$getCoverageDetails(id, token) + getCoverageDetails: (id, testId, token) => this.proxy.$getCoverageDetails(id, testId, token) .then(r => r.map(CoverageDetails.deserialize)), }); value.append(deserialized, tx); diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 180932b6d6d..09ff17ad867 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -14,7 +14,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IRequestService } from 'vs/platform/request/common/request'; +import { AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { IWorkspace, IWorkspaceContextService, WorkbenchState, isUntitledWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -223,6 +223,10 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return this._requestService.resolveProxy(url); } + $lookupAuthorization(authInfo: AuthInfo): Promise { + return this._requestService.lookupAuthorization(authInfo); + } + $loadCertificates(): Promise { return this._requestService.loadCertificates(); } diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 496651fe769..8b7c408c766 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -8,7 +8,7 @@ import * as objects from 'vs/base/common/objects'; import { Registry } from 'vs/platform/registry/common/platform'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IConfigurationNode, IConfigurationRegistry, Extensions, validateProperty, ConfigurationScope, OVERRIDE_PROPERTY_REGEX, IConfigurationDefaults, configurationDefaultsSchemaId, IConfigurationDelta, getDefaultValue } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationNode, IConfigurationRegistry, Extensions, validateProperty, ConfigurationScope, OVERRIDE_PROPERTY_REGEX, IConfigurationDefaults, configurationDefaultsSchemaId, IConfigurationDelta, getDefaultValue, getAllConfigurationProperties, parseScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { workspaceSettingsSchemaId, launchSchemaId, tasksSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { isObject, isUndefined } from 'vs/base/common/types'; @@ -160,11 +160,17 @@ defaultConfigurationExtPoint.setHandler((extensions, { added, removed }) => { const addedDefaultConfigurations = added.map(extension => { const overrides: IStringDictionary = objects.deepClone(extension.value); for (const key of Object.keys(overrides)) { + const registeredPropertyScheme = registeredProperties[key]; + if (registeredPropertyScheme?.disallowConfigurationDefault) { + extension.collector.warn(nls.localize('config.property.preventDefaultConfiguration.warning', "Cannot register configuration defaults for '{0}'. This setting does not allow contributing configuration defaults.", key)); + delete overrides[key]; + continue; + } if (!OVERRIDE_PROPERTY_REGEX.test(key)) { - const registeredPropertyScheme = registeredProperties[key]; if (registeredPropertyScheme?.scope && !allowedScopes.includes(registeredPropertyScheme.scope)) { extension.collector.warn(nls.localize('config.property.defaultConfiguration.warning', "Cannot register configuration defaults for '{0}'. Only defaults for machine-overridable, window, resource and language overridable scoped settings are supported.", key)); delete overrides[key]; + continue; } } } @@ -210,8 +216,7 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { const seenProperties = new Set(); - function handleConfiguration(node: IConfigurationNode, extension: IExtensionPointUser): IConfigurationNode[] { - const configurations: IConfigurationNode[] = []; + function handleConfiguration(node: IConfigurationNode, extension: IExtensionPointUser): IConfigurationNode { const configuration = objects.deepClone(node); if (configuration.title && (typeof configuration.title !== 'string')) { @@ -224,8 +229,7 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { configuration.extensionInfo = { id: extension.description.identifier.value, displayName: extension.description.displayName }; configuration.restrictedProperties = extension.description.capabilities?.untrustedWorkspaces?.supported === 'limited' ? extension.description.capabilities?.untrustedWorkspaces.restrictedConfigurations : undefined; configuration.title = configuration.title || extension.description.displayName || extension.description.identifier.value; - configurations.push(configuration); - return configurations; + return configuration; } function validateProperties(configuration: IConfigurationNode, extension: IExtensionPointUser): void { @@ -254,23 +258,7 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { continue; } seenProperties.add(key); - if (propertyConfiguration.scope) { - if (propertyConfiguration.scope.toString() === 'application') { - propertyConfiguration.scope = ConfigurationScope.APPLICATION; - } else if (propertyConfiguration.scope.toString() === 'machine') { - propertyConfiguration.scope = ConfigurationScope.MACHINE; - } else if (propertyConfiguration.scope.toString() === 'resource') { - propertyConfiguration.scope = ConfigurationScope.RESOURCE; - } else if (propertyConfiguration.scope.toString() === 'machine-overridable') { - propertyConfiguration.scope = ConfigurationScope.MACHINE_OVERRIDABLE; - } else if (propertyConfiguration.scope.toString() === 'language-overridable') { - propertyConfiguration.scope = ConfigurationScope.LANGUAGE_OVERRIDABLE; - } else { - propertyConfiguration.scope = ConfigurationScope.WINDOW; - } - } else { - propertyConfiguration.scope = ConfigurationScope.WINDOW; - } + propertyConfiguration.scope = propertyConfiguration.scope ? parseScope(propertyConfiguration.scope.toString()) : ConfigurationScope.WINDOW; } } const subNodes = configuration.allOf; @@ -288,9 +276,9 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { const configurations: IConfigurationNode[] = []; const value = extension.value; if (Array.isArray(value)) { - value.forEach(v => configurations.push(...handleConfiguration(v, extension))); + value.forEach(v => configurations.push(handleConfiguration(v, extension))); } else { - configurations.push(...handleConfiguration(value, extension)); + configurations.push(handleConfiguration(value, extension)); } extensionConfigurations.set(extension.description.identifier, configurations); addedConfigurations.push(...configurations); @@ -400,15 +388,11 @@ class SettingsTableRenderer extends Disposable implements IExtensionFeatureTable } render(manifest: IExtensionManifest): IRenderedData { - const configuration = manifest.contributes?.configuration; - let properties: any = {}; - if (Array.isArray(configuration)) { - configuration.forEach(config => { - properties = { ...properties, ...config.properties }; - }); - } else if (configuration) { - properties = configuration.properties; - } + const configuration: IConfigurationNode[] = manifest.contributes?.configuration + ? Array.isArray(manifest.contributes.configuration) ? manifest.contributes.configuration : [manifest.contributes.configuration] + : []; + + const properties = getAllConfigurationProperties(configuration); const contrib = properties ? Object.keys(properties) : []; const headers = [nls.localize('setting name', "ID"), nls.localize('description', "Description"), nls.localize('default', "Default")]; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 680d04cb5dd..b6b4f5746bb 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -14,7 +14,7 @@ import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; import { score, targetsNotebooks } from 'vs/editor/common/languageSelector'; import * as languageConfiguration from 'vs/editor/common/languages/languageConfiguration'; import { OverviewRulerLane } from 'vs/editor/common/model'; -import { ExtensionIdentifier, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as files from 'vs/platform/files/common/files'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILogService, ILoggerService, LogLevel } from 'vs/platform/log/common/log'; @@ -55,6 +55,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive'; import { ExtHostLabelService } from 'vs/workbench/api/common/extHostLabelService'; import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures'; +import { ExtHostLanguageModelTools } from 'vs/workbench/api/common/extHostLanguageModelTools'; import { IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels'; import { ExtHostLanguages } from 'vs/workbench/api/common/extHostLanguages'; import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService'; @@ -84,7 +85,7 @@ import { IExtHostTask } from 'vs/workbench/api/common/extHostTask'; import { ExtHostTelemetryLogger, IExtHostTelemetry, isNewAppInstall } from 'vs/workbench/api/common/extHostTelemetry'; import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { IExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration'; -import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; +import { IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline'; @@ -205,12 +206,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); - const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostLogService, extHostCommands, extHostDocumentsAndEditors)); + const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, accessor.get(IExtHostTesting)); const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol)); const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService)); - const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, initData.quality)); + const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments)); const extHostChatVariables = rpcProtocol.set(ExtHostContext.ExtHostChatVariables, new ExtHostChatVariables(rpcProtocol)); + const extHostLanguageModelTools = rpcProtocol.set(ExtHostContext.ExtHostLanguageModelTools, new ExtHostLanguageModelTools(rpcProtocol)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); const extHostStatusBar = rpcProtocol.set(ExtHostContext.ExtHostStatusBar, new ExtHostStatusBar(rpcProtocol, extHostCommands.converter)); @@ -287,11 +289,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I if (typeof options?.forceNewSession === 'object' && options.forceNewSession.learnMore) { checkProposedApiEnabled(extension, 'authLearnMore'); } + if (options?.account) { + checkProposedApiEnabled(extension, 'authGetSessions'); + } return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, - getSessions(providerId: string, scopes: readonly string[]) { + getAccounts(providerId: string) { checkProposedApiEnabled(extension, 'authGetSessions'); - return extHostAuthentication.getSessions(extension, providerId, scopes); + return extHostAuthentication.getAccounts(providerId); }, // TODO: remove this after GHPR and Codespaces move off of it async hasSession(providerId: string, scopes: readonly string[]) { @@ -1430,28 +1435,20 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatParticipantPrivate'); return extHostChatAgents2.createDynamicChatAgent(extension, id, dynamicProps, handler); }, - attachContext(name: string, value: string | vscode.Uri | vscode.Location | unknown, location: vscode.ChatLocation.Panel) { - checkProposedApiEnabled(extension, 'chatVariableResolver'); - return extHostChatVariables.attachContext(name, value, location); - } }; // namespace: lm const lm: typeof vscode.lm = { selectChatModels: (selector) => { - if (initData.quality === 'stable') { - console.warn(`[${ExtensionIdentifier.toKey(extension.identifier)}] This API is disabled in '${initData.environment.appName}'-stable.`); - return Promise.resolve([]); - } return extHostLanguageModels.selectLanguageModels(extension, selector ?? {}); }, onDidChangeChatModels: (listener, thisArgs?, disposables?) => { - if (initData.quality === 'stable') { - console.warn(`[${ExtensionIdentifier.toKey(extension.identifier)}] This API is disabled in '${initData.environment.appName}'-stable.`); - return Event.None(listener, thisArgs, disposables); - } return extHostLanguageModels.onDidChangeProviders(listener, thisArgs, disposables); }, + registerChatModelProvider: (id, provider, metadata) => { + checkProposedApiEnabled(extension, 'chatProvider'); + return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); + }, // --- embeddings get embeddingModels() { checkProposedApiEnabled(extension, 'embeddings'); @@ -1472,7 +1469,19 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } else { return extHostEmbeddings.computeEmbeddings(embeddingsModel, input, token); } - } + }, + registerTool(toolId: string, tool: vscode.LanguageModelTool) { + checkProposedApiEnabled(extension, 'lmTools'); + return extHostLanguageModelTools.registerTool(extension, toolId, tool); + }, + invokeTool(toolId: string, parameters: Object, token: vscode.CancellationToken) { + checkProposedApiEnabled(extension, 'lmTools'); + return extHostLanguageModelTools.invokeTool(toolId, parameters, token); + }, + get tools() { + checkProposedApiEnabled(extension, 'lmTools'); + return extHostLanguageModelTools.tools; + }, }; // namespace: speech @@ -1731,12 +1740,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, + ChatRequestEditorData: extHostTypes.ChatRequestEditorData, + ChatRequestNotebookData: extHostTypes.ChatRequestNotebookData, LanguageModelChatMessageRole: extHostTypes.LanguageModelChatMessageRole, LanguageModelChatMessage: extHostTypes.LanguageModelChatMessage, - LanguageModelChatMessage2: extHostTypes.LanguageModelChatMessage, // TODO@jrieken REMOVE - LanguageModelChatSystemMessage: extHostTypes.LanguageModelChatSystemMessage,// TODO@jrieken REMOVE - LanguageModelChatUserMessage: extHostTypes.LanguageModelChatUserMessage,// TODO@jrieken REMOVE - LanguageModelChatAssistantMessage: extHostTypes.LanguageModelChatAssistantMessage,// TODO@jrieken REMOVE + LanguageModelChatMessageFunctionResultPart: extHostTypes.LanguageModelFunctionResultPart, + LanguageModelChatResponseTextPart: extHostTypes.LanguageModelTextPart, + LanguageModelChatResponseFunctionUsePart: extHostTypes.LanguageModelFunctionUsePart, LanguageModelError: extHostTypes.LanguageModelError, NewSymbolName: extHostTypes.NewSymbolName, NewSymbolNameTag: extHostTypes.NewSymbolNameTag, diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index d01a3219f94..0427ebe7b17 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -31,6 +31,7 @@ import { ExtHostManagedSockets, IExtHostManagedSockets } from 'vs/workbench/api/ import { ExtHostAuthentication, IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { ExtHostLanguageModels, IExtHostLanguageModels } from 'vs/workbench/api/common/extHostLanguageModels'; import { IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration } from 'vs/workbench/api/common/extHostTerminalShellIntegration'; +import { ExtHostTesting, IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, InstantiationType.Delayed); registerSingleton(ILoggerService, ExtHostLoggerService, InstantiationType.Delayed); @@ -40,6 +41,7 @@ registerSingleton(IExtHostAuthentication, ExtHostAuthentication, InstantiationTy registerSingleton(IExtHostLanguageModels, ExtHostLanguageModels, InstantiationType.Eager); registerSingleton(IExtHostConfiguration, ExtHostConfiguration, InstantiationType.Eager); registerSingleton(IExtHostConsumerFileSystem, ExtHostConsumerFileSystem, InstantiationType.Eager); +registerSingleton(IExtHostTesting, ExtHostTesting, InstantiationType.Eager); registerSingleton(IExtHostDebugService, WorkerExtHostDebugService, InstantiationType.Eager); registerSingleton(IExtHostDecorations, ExtHostDecorations, InstantiationType.Eager); registerSingleton(IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors, InstantiationType.Eager); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2d10d6b7129..7bbab68a618 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -6,7 +6,6 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IRemoteConsoleLog } from 'vs/base/common/console'; -import { Location } from 'vs/editor/common/languages'; import { SerializedError } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -54,9 +53,10 @@ import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/cal import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IToolData, IToolDelta } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from 'vs/workbench/contrib/chat/common/languageModels'; -import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; +import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -69,7 +69,7 @@ import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFil import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { RelatedInformationResult, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; -import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; +import { AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProviderSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IExtensionDescriptionDelta, IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; @@ -82,6 +82,7 @@ import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from 'vs/workbench import * as search from 'vs/workbench/services/search/common/search'; import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import type { TerminalShellExecutionCommandLineConfidence } from 'vscode'; +import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; export interface IWorkspaceData extends IStaticWorkspaceData { folders: { uri: UriComponents; name: string; index: number }[]; @@ -144,10 +145,11 @@ export interface MainThreadCommentsShape extends IDisposable { $registerCommentController(handle: number, id: string, label: string, extensionId: string): void; $unregisterCommentController(handle: number): void; $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void; - $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, extensionId: ExtensionIdentifier, isTemplate: boolean, editorId?: string): languages.CommentThread | undefined; + $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, comments: languages.Comment[], extensionId: ExtensionIdentifier, isTemplate: boolean, editorId?: string): languages.CommentThread | undefined; $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, changes: CommentThreadChanges): void; $deleteCommentThread(handle: number, commentThreadHandle: number): void; $updateCommentingRanges(handle: number, resourceHints?: languages.CommentingRangeResourceHint): void; + $revealCommentThread(handle: number, commentThreadHandle: number, options: languages.CommentThreadRevealOptions): Promise; } export interface AuthenticationForceNewSessionOptions { @@ -161,7 +163,7 @@ export interface MainThreadAuthenticationShape extends IDisposable { $ensureProvider(id: string): Promise; $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void; $getSession(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean; forceNewSession?: boolean | AuthenticationForceNewSessionOptions; clearSessionPreference?: boolean }): Promise; - $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise; + $getAccounts(providerId: string): Promise>; $removeSession(providerId: string, sessionId: string): Promise; } @@ -1200,12 +1202,10 @@ export interface ExtHostSpeechShape { export interface MainThreadLanguageModelsShape extends IDisposable { $registerLanguageModelProvider(handle: number, identifier: string, metadata: ILanguageModelChatMetadata): void; $unregisterProvider(handle: number): void; - $handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise; - + $tryStartChatRequest(extension: ExtensionIdentifier, provider: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise; + $reportResponsePart(requestId: number, chunk: IChatResponseFragment): Promise; + $reportResponseDone(requestId: number, error: SerializedError | undefined): Promise; $selectChatModels(selector: ILanguageModelChatSelector): Promise; - - $fetchResponse(extension: ExtensionIdentifier, provider: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise; - $whenLanguageModelChatRequestMade(identifier: string, extension: ExtensionIdentifier, participant?: string, tokenCount?: number): void; $countTokens(provider: string, value: string | IChatMessage, token: CancellationToken): Promise; } @@ -1213,8 +1213,9 @@ export interface MainThreadLanguageModelsShape extends IDisposable { export interface ExtHostLanguageModelsShape { $acceptChatModelMetadata(data: ILanguageModelsChangeEvent): void; $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void; - $provideLanguageModelResponse(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; - $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise; + $startChatRequest(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; + $acceptResponsePart(requestId: number, chunk: IChatResponseFragment): Promise; + $acceptResponseDone(requestId: number, error: SerializedError | undefined): Promise; $provideTokenLength(handle: number, value: string | IChatMessage, token: CancellationToken): Promise; } @@ -1274,8 +1275,8 @@ export type IChatAgentHistoryEntryDto = { }; export interface ExtHostChatAgentsShape2 { - $invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; + $invokeAgent(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; + $provideFollowups(request: Dto, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $acceptFeedback(handle: number, result: IChatAgentResult, vote: ChatAgentVoteDirection, reportIssue?: boolean): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; $invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise; @@ -1291,7 +1292,13 @@ export interface MainThreadChatVariablesShape extends IDisposable { $registerVariable(handle: number, data: IChatVariableData): void; $handleProgressChunk(requestId: string, progress: IChatVariableResolverProgressDto): Promise; $unregisterVariable(handle: number): void; - $attachContext(name: string, value: string | Dto | URI | unknown, location: ChatAgentLocation): void; +} + +export interface MainThreadLanguageModelToolsShape extends IDisposable { + $getTools(): Promise; + $invokeTool(name: string, parameters: any, token: CancellationToken): Promise; + $registerTool(id: string): void; + $unregisterTool(name: string): void; } export type IChatRequestVariableValueDto = Dto; @@ -1300,6 +1307,11 @@ export interface ExtHostChatVariablesShape { $resolveVariable(handle: number, requestId: string, messageText: string, token: CancellationToken): Promise; } +export interface ExtHostLanguageModelToolsShape { + $acceptToolDelta(delta: IToolDelta): Promise; + $invokeTool(id: string, parameters: any, token: CancellationToken): Promise; +} + export interface MainThreadUrlsShape extends IDisposable { $registerUriHandler(handle: number, extensionId: ExtensionIdentifier, extensionDisplayName: string): Promise; $unregisterUriHandler(handle: number): Promise; @@ -1374,6 +1386,7 @@ export interface MainThreadWorkspaceShape extends IDisposable { $saveAll(includeUntitled?: boolean): Promise; $updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents; name?: string }[]): Promise; $resolveProxy(url: string): Promise; + $lookupAuthorization(authInfo: AuthInfo): Promise; $loadCertificates(): Promise; $requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; $registerEditSessionIdentityProvider(handle: number, scheme: string): void; @@ -1502,7 +1515,8 @@ export type SCMRawResourceSplices = [ export interface SCMHistoryItemGroupDto { readonly id: string; readonly name: string; - readonly base?: Omit; + readonly base?: Omit, 'remote'>; + readonly remote?: Omit, 'remote'>; } export interface SCMHistoryItemDto { @@ -1512,6 +1526,15 @@ export interface SCMHistoryItemDto { readonly author?: string; readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; readonly timestamp?: number; + readonly statistics?: { + readonly files: number; + readonly insertions: number; + readonly deletions: number; + }; + readonly labels?: { + readonly title: string; + readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; + }[]; } export interface SCMHistoryItemChangeDto { @@ -1523,24 +1546,24 @@ export interface SCMHistoryItemChangeDto { export interface MainThreadSCMShape extends IDisposable { $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): Promise; - $updateSourceControl(handle: number, features: SCMProviderFeatures): void; - $unregisterSourceControl(handle: number): void; + $updateSourceControl(handle: number, features: SCMProviderFeatures): Promise; + $unregisterSourceControl(handle: number): Promise; - $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): void; - $updateGroup(sourceControlHandle: number, handle: number, features: SCMGroupFeatures): void; - $updateGroupLabel(sourceControlHandle: number, handle: number, label: string): void; - $unregisterGroup(sourceControlHandle: number, handle: number): void; + $registerGroups(sourceControlHandle: number, groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures, /* multiDiffEditorEnableViewChanges */ boolean][], splices: SCMRawResourceSplices[]): Promise; + $updateGroup(sourceControlHandle: number, handle: number, features: SCMGroupFeatures): Promise; + $updateGroupLabel(sourceControlHandle: number, handle: number, label: string): Promise; + $unregisterGroup(sourceControlHandle: number, handle: number): Promise; - $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): void; + $spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): Promise; - $setInputBoxValue(sourceControlHandle: number, value: string): void; - $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void; - $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): void; - $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void; - $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): void; - $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void; + $setInputBoxValue(sourceControlHandle: number, value: string): Promise; + $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): Promise; + $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): Promise; + $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): Promise; + $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): Promise; + $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): Promise; - $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void; + $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise; } export interface MainThreadQuickDiffShape extends IDisposable { @@ -1567,6 +1590,7 @@ export interface IStartDebuggingOptions { suppressDebugStatusbar?: boolean; suppressDebugView?: boolean; suppressSaveBeforeStart?: boolean; + testRun?: IDebugTestRunReference; } export interface MainThreadDebugServiceShape extends IDisposable { @@ -1806,7 +1830,7 @@ export interface ExtHostLabelServiceShape { } export interface ExtHostAuthenticationShape { - $getSessions(id: string, scopes?: string[]): Promise>; + $getSessions(id: string, scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions): Promise>; $createSession(id: string, scopes: string[], options: IAuthenticationCreateSessionOptions): Promise; $removeSession(id: string, sessionId: string): Promise; $onDidChangeAuthenticationSessions(id: string, label: string): Promise; @@ -2166,6 +2190,7 @@ export interface ExtHostLanguageFeaturesShape { $resolveCompletionItem(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise; + $provideInlineEdits(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise; $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void; $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void; $freeInlineCompletionsList(handle: number, pid: number): void; @@ -2191,6 +2216,7 @@ export interface ExtHostLanguageFeaturesShape { $provideTypeHierarchySubtypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $releaseTypeHierarchy(handle: number, sessionId: string): void; $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise; + $releaseDocumentOnDropEdits(handle: number, cacheId: number): void; $provideMappedEdits(handle: number, document: UriComponents, codeBlocks: string[], context: IMappedEditsContextDto, token: CancellationToken): Promise; $provideInlineEdit(handle: number, document: UriComponents, context: languages.IInlineEditContext, token: CancellationToken): Promise; $freeInlineEdit(handle: number, pid: number): void; @@ -2302,6 +2328,7 @@ export interface ExtHostSCMShape { $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>; $setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise; $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise; + $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise; $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>; @@ -2706,7 +2733,7 @@ export interface ExtHostTestingShape { /** Expands a test item's children, by the given number of levels. */ $expandTest(testId: string, levels: number): Promise; /** Requests coverage details for a test run. Errors if not available. */ - $getCoverageDetails(coverageId: string, token: CancellationToken): Promise; + $getCoverageDetails(coverageId: string, testId: string | undefined, token: CancellationToken): Promise; /** Disposes resources associated with a test run. */ $disposeRun(runId: string): void; /** Configures a test run config. */ @@ -2812,6 +2839,7 @@ export const MainContext = { MainThreadEmbeddings: createProxyIdentifier('MainThreadEmbeddings'), MainThreadChatAgents2: createProxyIdentifier('MainThreadChatAgents2'), MainThreadChatVariables: createProxyIdentifier('MainThreadChatVariables'), + MainThreadLanguageModelTools: createProxyIdentifier('MainThreadChatSkills'), MainThreadClipboard: createProxyIdentifier('MainThreadClipboard'), MainThreadCommands: createProxyIdentifier('MainThreadCommands'), MainThreadComments: createProxyIdentifier('MainThreadComments'), @@ -2931,6 +2959,7 @@ export const ExtHostContext = { ExtHostInteractive: createProxyIdentifier('ExtHostInteractive'), ExtHostChatAgents2: createProxyIdentifier('ExtHostChatAgents'), ExtHostChatVariables: createProxyIdentifier('ExtHostChatVariables'), + ExtHostLanguageModelTools: createProxyIdentifier('ExtHostChatSkills'), ExtHostChatProvider: createProxyIdentifier('ExtHostChatProvider'), ExtHostSpeech: createProxyIdentifier('ExtHostSpeech'), ExtHostEmbeddings: createProxyIdentifier('ExtHostEmbeddings'), diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 16b1d4a405b..c5ba402ef07 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -32,7 +32,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; private _getSessionTaskSingler = new TaskSingler(); - private _getSessionsTaskSingler = new TaskSingler>(); constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService @@ -54,14 +53,9 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { }); } - async getSessions(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[]): Promise> { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - const sortedScopes = [...scopes].sort().join(' '); - return await this._getSessionsTaskSingler.getOrCreate(`${extensionId} ${sortedScopes}`, async () => { - await this._proxy.$ensureProvider(providerId); - const extensionName = requestingExtension.displayName || requestingExtension.name; - return this._proxy.$getSessions(providerId, scopes, extensionId, extensionName); - }); + async getAccounts(providerId: string) { + await this._proxy.$ensureProvider(providerId); + return await this._proxy.$getAccounts(providerId); } async removeSession(providerId: string, sessionId: string): Promise { @@ -89,7 +83,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { }); } - async $createSession(providerId: string, scopes: string[], options: vscode.AuthenticationProviderCreateSessionOptions): Promise { + async $createSession(providerId: string, scopes: string[], options: vscode.AuthenticationProviderSessionOptions): Promise { const providerData = this._authenticationProviders.get(providerId); if (providerData) { return await providerData.provider.createSession(scopes, options); @@ -107,10 +101,10 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } - async $getSessions(providerId: string, scopes?: string[]): Promise> { + async $getSessions(providerId: string, scopes: ReadonlyArray | undefined, options: vscode.AuthenticationProviderSessionOptions): Promise> { const providerData = this._authenticationProviders.get(providerId); if (providerData) { - return await providerData.provider.getSessions(scopes); + return await providerData.provider.getSessions(scopes, options); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 435c4c45f69..8b2209b5363 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -11,6 +11,7 @@ import { Emitter } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; import { Disposable, DisposableMap, DisposableStore } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -19,10 +20,11 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IChatProgressDto, IExtensionChatAgentMetadata, IMainContext, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatContentReference, IChatFollowup, IChatUserActionEvent, ChatAgentVoteDirection, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatResponseErrorDetails, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; @@ -262,8 +264,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS constructor( mainContext: IMainContext, private readonly _logService: ILogService, - private readonly commands: ExtHostCommands, - private readonly quality: string | undefined + private readonly _commands: ExtHostCommands, + private readonly _documents: ExtHostDocuments ) { super(); this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2); @@ -275,31 +277,30 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS createChatAgent(extension: IExtensionDescription, id: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { const handle = ExtHostChatAgents2._idPool++; - const agent = new ExtHostChatAgent(extension, this.quality, id, this._proxy, handle, handler); + const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); this._agents.set(handle, agent); - if (agent.isAgentEnabled()) { - this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined); - } - + this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined); return agent.apiAgent; } createDynamicChatAgent(extension: IExtensionDescription, id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { const handle = ExtHostChatAgents2._idPool++; - const agent = new ExtHostChatAgent(extension, this.quality, id, this._proxy, handle, handler); + const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); this._agents.set(handle, agent); this._proxy.$registerAgent(handle, extension.identifier, id, { isSticky: true } satisfies IExtensionChatAgentMetadata, dynamicProps); return agent.apiAgent; } - async $invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { + async $invokeAgent(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { throw new Error(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`); } + const request = revive(requestDto); + // Init session disposables let sessionDisposables = this._sessionDisposables.get(request.sessionId); if (!sessionDisposables) { @@ -307,11 +308,28 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS this._sessionDisposables.set(request.sessionId, sessionDisposables); } - const stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this.commands.converter, sessionDisposables); + const stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables); try { const convertedHistory = await this.prepareHistoryTurns(request.agentId, context); + + // in-place converting for location-data + let location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined; + if (request.locationData?.type === ChatAgentLocation.Editor) { + // editor data + const document = this._documents.getDocument(request.locationData.document); + location2 = new extHostTypes.ChatRequestEditorData(document, typeConvert.Selection.to(request.locationData.selection), typeConvert.Range.to(request.locationData.wholeRange)); + + } else if (request.locationData?.type === ChatAgentLocation.Notebook) { + // notebook data + const cell = this._documents.getDocument(request.locationData.sessionInputUri); + location2 = new extHostTypes.ChatRequestNotebookData(cell); + + } else if (request.locationData?.type === ChatAgentLocation.Terminal) { + // TBD + } + const task = agent.invoke( - typeConvert.ChatAgentRequest.to(request), + typeConvert.ChatAgentRequest.to(request, location2), { history: convertedHistory }, stream.apiObject, token @@ -342,6 +360,11 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS }), token); } catch (e) { this._logService.error(e, agent.extension); + + if (e instanceof extHostTypes.LanguageModelError && e.cause) { + e = e.cause; + } + return { errorDetails: { message: toErrorMessage(e), responseIsIncomplete: true } }; } finally { @@ -363,7 +386,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentValueReference.to), h.request.agentId)); // RESPONSE turn - const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.toContent(r, this.commands.converter))); + const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.toContent(r, this._commands.converter))); res.push(new extHostTypes.ChatResponseTurn(parts, result, h.request.agentId, h.request.command)); } @@ -374,12 +397,13 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS this._sessionDisposables.deleteAndDispose(sessionId); } - async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { + async $provideFollowups(requestDto: Dto, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { return Promise.resolve([]); } + const request = revive(requestDto); const convertedHistory = await this.prepareHistoryTurns(agent.id, context); const ehResult = typeConvert.ChatAgentResult.to(result); @@ -428,7 +452,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return; } - const ehAction = typeConvert.ChatAgentUserActionEvent.to(result, event, this.commands.converter); + const ehAction = typeConvert.ChatAgentUserActionEvent.to(result, event, this._commands.converter); if (ehAction) { agent.acceptAction(Object.freeze(ehAction)); } @@ -451,7 +475,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const items = await agent.invokeCompletionProvider(query, token); - return items.map((i) => typeConvert.ChatAgentCompletionItem.from(i, this.commands.converter, disposables)); + return items.map((i) => typeConvert.ChatAgentCompletionItem.from(i, this._commands.converter, disposables)); } async $provideWelcomeMessage(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined> { @@ -493,7 +517,6 @@ class ExtHostChatAgent { constructor( public readonly extension: IExtensionDescription, - private readonly quality: string | undefined, public readonly id: string, private readonly _proxy: MainThreadChatAgentsShape2, private readonly _handle: number, @@ -516,11 +539,6 @@ class ExtHostChatAgent { return await this._agentVariableProvider.provider.provideCompletionItems(query, token) ?? []; } - public isAgentEnabled() { - // If in stable and this extension doesn't have the right proposed API, then don't register the agent - return !(this.quality === 'stable' && !isProposedApiEnabled(this.extension, 'chatParticipantPrivate')); - } - async provideFollowups(result: vscode.ChatResult, context: vscode.ChatContext, token: CancellationToken): Promise { if (!this._followupProvider) { return []; @@ -578,10 +596,6 @@ class ExtHostChatAgent { } updateScheduled = true; queueMicrotask(() => { - if (!that.isAgentEnabled()) { - return; - } - this._proxy.$updateAgent(this._handle, { icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index 5f0bf7d2449..dfc37201bd4 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -64,10 +64,6 @@ export class ExtHostChatVariables implements ExtHostChatVariablesShape { this._proxy.$unregisterVariable(handle); }); } - - attachContext(name: string, value: string | vscode.Location | vscode.Uri | unknown, location: vscode.ChatLocation.Panel) { - this._proxy.$attachContext(name, extHostTypes.Location.isLocation(value) ? typeConvert.Location.from(value) : value, typeConvert.ChatLocation.from(location)); - } } class ChatVariableResolverResponseStream { diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index b3f54666152..c84fb4782f9 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -215,7 +215,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } else if (rangesResult) { ranges = { ranges: rangesResult.ranges || [], - fileComments: rangesResult.fileComments || false + fileComments: rangesResult.enableFileComments || false }; } else { ranges = rangesResult ?? undefined; @@ -424,6 +424,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this._id, this._uri, extHostTypeConverter.Range.from(this._range), + this._comments.map(cmt => convertToDTOComment(this, cmt, this._commentsMap, this.extensionDescription)), extensionDescription.identifier, this._isTemplate, editorId @@ -436,9 +437,6 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this.eventuallyUpdateCommentThread(); })); - // set up comments after ctor to batch update events. - this.comments = _comments; - this._localDisposables.push({ dispose: () => { proxy.$deleteCommentThread( @@ -465,6 +463,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo set label(value: string | undefined) { that.label = value; }, get state(): vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined { return that.state; }, set state(value: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability }) { that.state = value; }, + reveal: (options?: vscode.CommentThreadRevealOptions) => that.reveal(options), dispose: () => { that.dispose(); } @@ -548,6 +547,11 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return; } + async reveal(options?: vscode.CommentThreadRevealOptions): Promise { + checkProposedApiEnabled(this.extensionDescription, 'commentReveal'); + return proxy.$revealCommentThread(this._commentControllerHandle, this.handle, { preserveFocus: false, focusReply: false, ...options }); + } + dispose() { this._isDiposed = true; this._acceptInputDisposables.dispose(); diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index d5ea2bdf817..a5e22aa3dee 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -7,7 +7,8 @@ import { asPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { Disposable as DisposableCls, toDisposable } from 'vs/base/common/lifecycle'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ISignService } from 'vs/platform/sign/common/sign'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -25,11 +26,11 @@ import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; import { IExtHostConfiguration } from '../common/extHostConfiguration'; import { IExtHostVariableResolverProvider } from './extHostVariableResolverService'; -import { toDisposable } from 'vs/base/common/lifecycle'; import { ThemeIcon as ThemeIconUtils } from 'vs/base/common/themables'; import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; import { coalesce } from 'vs/base/common/arrays'; +import { IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; export const IExtHostDebugService = createDecorator('IExtHostDebugService'); @@ -60,7 +61,7 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { asDebugSourceUri(source: vscode.DebugProtocolSource, session?: vscode.DebugSession): vscode.Uri; } -export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDebugServiceShape { +export abstract class ExtHostDebugServiceBase extends DisposableCls implements IExtHostDebugService, ExtHostDebugServiceShape { readonly _serviceBrand: undefined; @@ -123,7 +124,10 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs, @IExtHostVariableResolverProvider private _variableResolver: IExtHostVariableResolverProvider, @IExtHostCommands private _commands: IExtHostCommands, + @IExtHostTesting private _testing: IExtHostTesting, ) { + super(); + this._configProviderHandleCounter = 0; this._configProviders = []; @@ -136,25 +140,25 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E this._debugAdapters = new Map(); this._debugAdaptersTrackers = new Map(); - this._onDidStartDebugSession = new Emitter(); - this._onDidTerminateDebugSession = new Emitter(); - this._onDidChangeActiveDebugSession = new Emitter(); - this._onDidReceiveDebugSessionCustomEvent = new Emitter(); + this._onDidStartDebugSession = this._register(new Emitter()); + this._onDidTerminateDebugSession = this._register(new Emitter()); + this._onDidChangeActiveDebugSession = this._register(new Emitter()); + this._onDidReceiveDebugSessionCustomEvent = this._register(new Emitter()); this._debugServiceProxy = extHostRpcService.getProxy(MainContext.MainThreadDebugService); - this._onDidChangeBreakpoints = new Emitter(); + this._onDidChangeBreakpoints = this._register(new Emitter()); - this._onDidChangeActiveStackItem = new Emitter(); + this._onDidChangeActiveStackItem = this._register(new Emitter()); this._activeDebugConsole = new ExtHostDebugConsole(this._debugServiceProxy); this._breakpoints = new Map(); this._extensionService.getExtensionRegistry().then((extensionRegistry: ExtensionDescriptionRegistry) => { - extensionRegistry.onDidChange(_ => { + this._register(extensionRegistry.onDidChange(_ => { this.registerAllDebugTypes(extensionRegistry); - }); + })); this.registerAllDebugTypes(extensionRegistry); }); } @@ -169,7 +173,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E return item ? this.convertVisualizerTreeItem(treeId, item) : undefined; } - public registerDebugVisualizationTree(manifest: Readonly, id: string, provider: vscode.DebugVisualizationTree): vscode.Disposable { + public registerDebugVisualizationTree(manifest: IExtensionDescription, id: string, provider: vscode.DebugVisualizationTree): vscode.Disposable { const extensionId = ExtensionIdentifier.toKey(manifest.identifier); const key = this.extensionVisKey(extensionId, id); if (this._debugVisualizationProviders.has(key)) { @@ -464,6 +468,8 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E } public startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, options: vscode.DebugSessionOptions): Promise { + const testRunMeta = options.testRun && this._testing.getMetadataForRun(options.testRun); + return this._debugServiceProxy.$startDebugging(folder ? folder.uri : undefined, nameOrConfig, { parentSessionID: options.parentSession ? options.parentSession.id : undefined, lifecycleManagedByParent: options.lifecycleManagedByParent, @@ -471,6 +477,10 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E noDebug: options.noDebug, compact: options.compact, suppressSaveBeforeStart: options.suppressSaveBeforeStart, + testRun: testRunMeta && { + runId: testRunMeta.runId, + taskId: testRunMeta.taskId, + }, // Check debugUI for back-compat, #147264 suppressDebugStatusbar: options.suppressDebugStatusbar ?? (options as any).debugUI?.simple, @@ -1245,8 +1255,9 @@ export class WorkerExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostConfiguration configurationService: IExtHostConfiguration, @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider, - @IExtHostCommands commands: IExtHostCommands + @IExtHostCommands commands: IExtHostCommands, + @IExtHostTesting testing: IExtHostTesting, ) { - super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands, testing); } } diff --git a/src/vs/workbench/api/common/extHostDiagnostics.ts b/src/vs/workbench/api/common/extHostDiagnostics.ts index e23ce395cc8..da9a087fbb1 100644 --- a/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -234,7 +234,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { private static _idPool: number = 0; private static readonly _maxDiagnosticsPerFile: number = 1000; - private static readonly _maxDiagnosticsTotal: number = 1.1 * ExtHostDiagnostics._maxDiagnosticsPerFile; + private static readonly _maxDiagnosticsTotal: number = 1.1 * this._maxDiagnosticsPerFile; private readonly _proxy: MainThreadDiagnosticsShape; private readonly _collections = new Map(); diff --git a/src/vs/workbench/api/common/extHostDialogs.ts b/src/vs/workbench/api/common/extHostDialogs.ts index c33cb0704dd..372037aa341 100644 --- a/src/vs/workbench/api/common/extHostDialogs.ts +++ b/src/vs/workbench/api/common/extHostDialogs.ts @@ -7,7 +7,7 @@ import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { MainContext, MainThreadDiaglogsShape, IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; -import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class ExtHostDialogs { @@ -17,7 +17,7 @@ export class ExtHostDialogs { this._proxy = mainContext.getProxy(MainContext.MainThreadDialogs); } - showOpenDialog(extension: IRelaxedExtensionDescription, options?: vscode.OpenDialogOptions): Promise { + showOpenDialog(extension: IExtensionDescription, options?: vscode.OpenDialogOptions): Promise { if (options?.allowUIResources) { checkProposedApiEnabled(extension, 'showLocal'); } diff --git a/src/vs/workbench/api/common/extHostDocumentData.ts b/src/vs/workbench/api/common/extHostDocumentData.ts index ee321b2575a..b3edbad94a1 100644 --- a/src/vs/workbench/api/common/extHostDocumentData.ts +++ b/src/vs/workbench/api/common/extHostDocumentData.ts @@ -76,6 +76,9 @@ export class ExtHostDocumentData extends MirrorTextModel { validateRange(ran) { return that._validateRange(ran); }, validatePosition(pos) { return that._validatePosition(pos); }, getWordRangeAtPosition(pos, regexp?) { return that._getWordRangeAtPosition(pos, regexp); }, + [Symbol.for('debug.description')]() { + return `TextDocument(${that._uri.toString()})`; + } }; } return Object.freeze(this._document); diff --git a/src/vs/workbench/api/common/extHostEmbedding.ts b/src/vs/workbench/api/common/extHostEmbedding.ts index f99cd387c62..4c712aee9e8 100644 --- a/src/vs/workbench/api/common/extHostEmbedding.ts +++ b/src/vs/workbench/api/common/extHostEmbedding.ts @@ -39,6 +39,7 @@ export class ExtHostEmbeddings implements ExtHostEmbeddingsShape { this._provider.set(handle, { id: embeddingsModel, provider }); return toDisposable(() => { + this._allKnownModels.delete(embeddingsModel); this._proxy.$unregisterEmbeddingProvider(handle); this._provider.delete(handle); }); diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 4ea250c3bf8..97935e89d08 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -24,7 +24,7 @@ import { MissingExtensionDependency, ActivationKind, checkProposedApiEnabled, is import { ExtensionDescriptionRegistry, IActivationEventsReader } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as errors from 'vs/base/common/errors'; import type * as vscode from 'vscode'; -import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { VSBuffer } from 'vs/base/common/buffer'; import { ExtensionGlobalMemento, ExtensionMemento } from 'vs/workbench/api/common/extHostMemento'; import { RemoteAuthorityResolverError, ExtensionKind, ExtensionMode, ExtensionRuntime, ManagedResolvedAuthority as ExtHostManagedResolvedAuthority } from 'vs/workbench/api/common/extHostTypes'; @@ -498,9 +498,10 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private _loadExtensionContext(extensionDescription: IExtensionDescription): Promise { const lanuageModelAccessInformation = this._extHostLanguageModels.createLanguageModelAccessInformation(extensionDescription); - const globalState = new ExtensionGlobalMemento(extensionDescription, this._storage); - const workspaceState = new ExtensionMemento(extensionDescription.identifier.value, false, this._storage); - const secrets = new ExtensionSecrets(extensionDescription, this._secretState); + // TODO: These should probably be disposed when the extension deactivates + const globalState = this._register(new ExtensionGlobalMemento(extensionDescription, this._storage)); + const workspaceState = this._register(new ExtensionMemento(extensionDescription.identifier.value, false, this._storage)); + const secrets = this._register(new ExtensionSecrets(extensionDescription, this._secretState)); const extensionMode = extensionDescription.isUnderDevelopment ? (this._initData.environment.extensionTestsLocationURI ? ExtensionMode.Test : ExtensionMode.Development) : ExtensionMode.Production; @@ -615,7 +616,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }); } - private _activateAllStartupFinishedDeferred(extensions: Readonly[], start: number = 0): void { + private _activateAllStartupFinishedDeferred(extensions: IExtensionDescription[], start: number = 0): void { const timeBudget = 50; // 50 milliseconds const startTime = Date.now(); @@ -1230,7 +1231,7 @@ class SyncedActivationEventsReader implements IActivationEventsReader { this.addActivationEvents(activationEvents); } - public readActivationEvents(extensionDescription: Readonly): string[] { + public readActivationEvents(extensionDescription: IExtensionDescription): string[] { return this._map.get(extensionDescription.identifier) ?? []; } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 215ff5fda37..0706a87cedb 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -33,7 +33,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostTelemetry, IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import { CodeActionKind, CompletionList, Disposable, DocumentDropOrPasteEditKind, DocumentSymbol, InlineCompletionTriggerKind, InlineEditTriggerKind, InternalDataTransferItem, Location, NewSymbolNameTriggerKind, Range, SemanticTokens, SemanticTokensEdit, SemanticTokensEdits, SnippetString, SymbolInformation, SyntaxTokenType } from 'vs/workbench/api/common/extHostTypes'; -import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; import { Cache } from './cache'; import * as extHostProtocol from './extHost.protocol'; @@ -501,9 +501,9 @@ class CodeActionAdapter { } else { if (codeActionContext.only) { if (!candidate.kind) { - this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); + this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); } else if (!codeActionContext.only.contains(candidate.kind)) { - this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); + this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value}' requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); } } @@ -1287,6 +1287,10 @@ class InlineCompletionAdapterBase { return undefined; } + async provideInlineEdits(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + return undefined; + } + disposeCompletions(pid: number): void { } handleDidShowCompletionItem(pid: number, idx: number, updatedInsertText: string): void { } @@ -1392,6 +1396,82 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { }; } + override async provideInlineEdits(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + if (!this._provider.provideInlineEdits) { + return undefined; + } + checkProposedApiEnabled(this._extension, 'inlineCompletionsAdditions'); + + const doc = this._documents.getDocument(resource); + const r = typeConvert.Range.to(range); + + const result = await this._provider.provideInlineEdits(doc, r, { + selectedCompletionInfo: + context.selectedSuggestionInfo + ? { + range: typeConvert.Range.to(context.selectedSuggestionInfo.range), + text: context.selectedSuggestionInfo.text + } + : undefined, + triggerKind: this.languageTriggerKindToVSCodeTriggerKind[context.triggerKind], + userPrompt: context.userPrompt, + }, token); + + if (!result) { + // undefined and null are valid results + return undefined; + } + + if (token.isCancellationRequested) { + // cancelled -> return without further ado, esp no caching + // of results as they will leak + return undefined; + } + + const normalizedResult = Array.isArray(result) ? result : result.items; + const commands = this._isAdditionsProposedApiEnabled ? Array.isArray(result) ? [] : result.commands || [] : []; + const enableForwardStability = this._isAdditionsProposedApiEnabled && !Array.isArray(result) ? result.enableForwardStability : undefined; + + let disposableStore: DisposableStore | undefined = undefined; + const pid = this._references.createReferenceId({ + dispose() { + disposableStore?.dispose(); + }, + items: normalizedResult + }); + + return { + pid, + items: normalizedResult.map((item, idx) => { + let command: languages.Command | undefined = undefined; + if (item.command) { + if (!disposableStore) { + disposableStore = new DisposableStore(); + } + command = this._commands.toInternal(item.command, disposableStore); + } + + const insertText = item.insertText; + return ({ + insertText: typeof insertText === 'string' ? insertText : { snippet: insertText.value }, + filterText: item.filterText, + range: item.range ? typeConvert.Range.from(item.range) : undefined, + command, + idx: idx, + completeBracketPairs: this._isAdditionsProposedApiEnabled ? item.completeBracketPairs : false, + }); + }), + commands: commands.map(c => { + if (!disposableStore) { + disposableStore = new DisposableStore(); + } + return this._commands.toInternal(c, disposableStore); + }), + suppressSuggestions: false, + enableForwardStability, + }; + } + override disposeCompletions(pid: number) { const data = this._references.disposeReferenceId(pid); data?.dispose(); @@ -2581,6 +2661,10 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, InlineCompletionAdapterBase, adapter => adapter.provideInlineCompletions(URI.revive(resource), position, context, token), undefined, token); } + $provideInlineEdits(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + return this._withAdapter(handle, InlineCompletionAdapterBase, adapter => adapter.provideInlineEdits(URI.revive(resource), range, context, token), undefined, token); + } + $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void { this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { adapter.handleDidShowCompletionItem(pid, idx, updatedInsertText); @@ -2806,7 +2890,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, DocumentDropEditAdapter, adapter => adapter.resolveDropEdit(id, token), {}, undefined); } - $releaseDropEdits(handle: number, cacheId: number): void { + $releaseDocumentOnDropEdits(handle: number, cacheId: number): void { this._withAdapter(handle, DocumentDropEditAdapter, adapter => Promise.resolve(adapter.releaseDropEdits(cacheId)), undefined, undefined); } diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts new file mode 100644 index 00000000000..e588f50ab6d --- /dev/null +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtHostLanguageModelToolsShape, IMainContext, MainContext, MainThreadLanguageModelToolsShape } from 'vs/workbench/api/common/extHost.protocol'; +import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; +import { IToolData, IToolDelta } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import type * as vscode from 'vscode'; + +export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { + /** A map of tools that were registered in this EH */ + private readonly _registeredTools = new Map(); + private readonly _proxy: MainThreadLanguageModelToolsShape; + + /** A map of all known tools, from other EHs or registered in vscode core */ + private readonly _allTools = new Map(); + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageModelTools); + + this._proxy.$getTools().then(tools => { + for (const tool of tools) { + this._allTools.set(tool.name, tool); + } + }); + } + + async invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + // Making the round trip here because not all tools were necessarily registered in this EH + return await this._proxy.$invokeTool(name, parameters, token); + } + + async $acceptToolDelta(delta: IToolDelta): Promise { + if (delta.added) { + this._allTools.set(delta.added.name, delta.added); + } + + if (delta.removed) { + this._allTools.delete(delta.removed); + } + } + + get tools(): vscode.LanguageModelToolDescription[] { + return Array.from(this._allTools.values()) + .map(tool => typeConvert.LanguageModelToolDescription.to(tool)); + } + + async $invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + const item = this._registeredTools.get(name); + if (!item) { + throw new Error(`Unknown tool ${name}`); + } + + return await item.tool.invoke(parameters, token); + } + + registerTool(extension: IExtensionDescription, name: string, tool: vscode.LanguageModelTool): IDisposable { + this._registeredTools.set(name, { extension, tool }); + this._proxy.$registerTool(name); + + return toDisposable(() => { + this._registeredTools.delete(name); + this._proxy.$unregisterTool(name); + }); + } +} diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 97ee59fa601..b67999350af 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AsyncIterableSource, Barrier } from 'vs/base/common/async'; +import { AsyncIterableObject, AsyncIterableSource } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { CancellationError } from 'vs/base/common/errors'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { CancellationError, SerializedError, transformErrorForSerialization, transformErrorFromSerialization } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -19,7 +20,7 @@ import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentic import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IChatMessage, IChatResponseFragment, IChatResponsePart, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; import { INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; @@ -36,13 +37,13 @@ type LanguageModelData = { class LanguageModelResponseStream { - readonly stream = new AsyncIterableSource(); + readonly stream = new AsyncIterableSource(); constructor( readonly option: number, - stream?: AsyncIterableSource + stream?: AsyncIterableSource ) { - this.stream = stream ?? new AsyncIterableSource(); + this.stream = stream ?? new AsyncIterableSource(); } } @@ -51,17 +52,26 @@ class LanguageModelResponse { readonly apiObject: vscode.LanguageModelChatResponse; private readonly _responseStreams = new Map(); - private readonly _defaultStream = new AsyncIterableSource(); + private readonly _defaultStream = new AsyncIterableSource(); private _isDone: boolean = false; - private _isStreaming: boolean = false; constructor() { const that = this; this.apiObject = { // result: promise, - text: that._defaultStream.asyncIterable, - // streams: AsyncIterable[] // FUTURE responses per N + get stream() { + return that._defaultStream.asyncIterable; + }, + get text() { + return AsyncIterableObject.map(that._defaultStream.asyncIterable, part => { + if (part instanceof extHostTypes.LanguageModelTextPart) { + return part.value; + } else { + return undefined; + } + }).coalesce(); + }, }; } @@ -79,7 +89,6 @@ class LanguageModelResponse { if (this._isDone) { return; } - this._isStreaming = true; let res = this._responseStreams.get(fragment.index); if (!res) { if (this._responseStreams.size === 0) { @@ -90,12 +99,16 @@ class LanguageModelResponse { } this._responseStreams.set(fragment.index, res); } - res.stream.emitOne(fragment.part); + + let out: vscode.LanguageModelChatResponseTextPart | vscode.LanguageModelChatResponseFunctionUsePart; + if (fragment.part.type === 'text') { + out = new extHostTypes.LanguageModelTextPart(fragment.part.value); + } else { + out = new extHostTypes.LanguageModelFunctionUsePart(fragment.part.name, fragment.part.parameters); + } + res.stream.emitOne(out); } - get isStreaming(): boolean { - return this._isStreaming; - } reject(err: Error): void { this._isDone = true; @@ -176,28 +189,65 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { }); } - async $provideLanguageModelResponse(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise { + async $startChatRequest(handle: number, requestId: number, from: ExtensionIdentifier, messages: IChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken): Promise { const data = this._languageModels.get(handle); if (!data) { - return; + throw new Error('Provider not found'); } - const progress = new Progress(async fragment => { + const progress = new Progress(async fragment => { if (token.isCancellationRequested) { this._logService.warn(`[CHAT](${data.extension.value}) CANNOT send progress because the REQUEST IS CANCELLED`); return; } - this._proxy.$handleProgressChunk(requestId, { index: fragment.index, part: fragment.part }); + + let part: IChatResponsePart | undefined; + if (fragment.part instanceof extHostTypes.LanguageModelFunctionUsePart) { + part = { type: 'function_use', name: fragment.part.name, parameters: fragment.part.parameters }; + } else if (fragment.part instanceof extHostTypes.LanguageModelTextPart) { + part = { type: 'text', value: fragment.part.value }; + } + + if (!part) { + this._logService.warn(`[CHAT](${data.extension.value}) UNKNOWN part ${JSON.stringify(fragment)}`); + return; + } + + this._proxy.$reportResponsePart(requestId, { index: fragment.index, part }); }); - return data.provider.provideLanguageModelResponse( - messages.map(typeConvert.LanguageModelChatMessage.to), - options, - ExtensionIdentifier.toKey(from), - progress, - token - ); - } + let p: Promise; + if (data.provider.provideLanguageModelResponse2) { + + p = Promise.resolve(data.provider.provideLanguageModelResponse2( + messages.map(typeConvert.LanguageModelChatMessage.to), + options, + ExtensionIdentifier.toKey(from), + progress, + token + )); + + } else { + + const progress2 = new Progress(async fragment => { + progress.report({ index: fragment.index, part: new extHostTypes.LanguageModelTextPart(fragment.part) }); + }); + + p = Promise.resolve(data.provider.provideLanguageModelResponse( + messages.map(typeConvert.LanguageModelChatMessage.to), + options?.modelOptions ?? {}, + ExtensionIdentifier.toKey(from), + progress2, + token + )); + } + + p.then(() => { + this._proxy.$reportResponseDone(requestId, undefined); + }, err => { + this._proxy.$reportResponseDone(requestId, transformErrorForSerialization(err)); + }); + } //#region --- token counting @@ -253,6 +303,11 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { continue; } + // make sure auth information is correct + if (this._isUsingAuth(extension.identifier, data.metadata)) { + await this._fakeAuthPopulate(data.metadata); + } + let apiObject = data.apiObjects.get(extension.identifier); if (!apiObject) { @@ -306,45 +361,33 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } } - const requestId = (Math.random() * 1e6) | 0; - const requestPromise = this._proxy.$fetchResponse(from, languageModelId, requestId, internalMessages, options.modelOptions ?? {}, token); + try { + const requestId = (Math.random() * 1e6) | 0; + const res = new LanguageModelResponse(); + this._pendingRequest.set(requestId, { languageModelId, res }); - const barrier = new Barrier(); + try { + await this._proxy.$tryStartChatRequest(from, languageModelId, requestId, internalMessages, options, token); - const res = new LanguageModelResponse(); - this._pendingRequest.set(requestId, { languageModelId, res }); - - let error: Error | undefined; - - requestPromise.catch(err => { - if (barrier.isOpen()) { - // we received an error while streaming. this means we need to reject the "stream" - // because we have already returned the request object - res.reject(err); - } else { - error = err; - } - }).finally(() => { - this._pendingRequest.delete(requestId); - res.resolve(); - barrier.open(); - }); - - await barrier.wait(); - - if (error) { - if (error.name === extHostTypes.LanguageModelError.name) { + } catch (error) { + // error'ing here means that the request could NOT be started/made, e.g. wrong model, no access, etc, but + // later the response can fail as well. Those failures are communicated via the stream-object + this._pendingRequest.delete(requestId); throw error; } + return res.apiObject; + + } catch (error) { + if (error.name === extHostTypes.LanguageModelError.name) { + throw error; + } throw new extHostTypes.LanguageModelError( - `Language model '${languageModelId}' errored, check cause for more details`, + `Language model '${languageModelId}' errored: ${toErrorMessage(error)}`, 'Unknown', error ); } - - return res.apiObject; } private _convertMessages(extension: IExtensionDescription, messages: vscode.LanguageModelChatMessage[]) { @@ -353,18 +396,36 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { if (message.role as number === extHostTypes.LanguageModelChatMessageRole.System) { checkProposedApiEnabled(extension, 'languageModelSystem'); } + if (message.content2 instanceof extHostTypes.LanguageModelFunctionResultPart) { + checkProposedApiEnabled(extension, 'lmTools'); + } internalMessages.push(typeConvert.LanguageModelChatMessage.from(message)); } return internalMessages; } - async $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise { - const data = this._pendingRequest.get(requestId);//.report(chunk); + async $acceptResponsePart(requestId: number, chunk: IChatResponseFragment): Promise { + const data = this._pendingRequest.get(requestId); if (data) { data.res.handleFragment(chunk); } } + async $acceptResponseDone(requestId: number, error: SerializedError | undefined): Promise { + const data = this._pendingRequest.get(requestId); + if (!data) { + return; + } + this._pendingRequest.delete(requestId); + if (error) { + // we error the stream because that's the only way to signal + // that the request has failed + data.res.reject(transformErrorFromSerialization(error)); + } else { + data.res.resolve(); + } + } + // BIG HACK: Using AuthenticationProviders to check access to Language Models private async _getAuthAccess(from: IExtensionDescription, to: { identifier: ExtensionIdentifier; displayName: string }, justification: string | undefined, silent: boolean | undefined): Promise { // This needs to be done in both MainThread & ExtHost ChatProvider @@ -403,6 +464,10 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { private async _fakeAuthPopulate(metadata: ILanguageModelChatMetadata): Promise { + if (!metadata.auth) { + return; + } + for (const from of this._languageAccessInformationExtensions) { try { await this._getAuthAccess(from, { identifier: metadata.extension, displayName: '' }, undefined, true); diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 3a7e105c843..5b707fc865e 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -36,7 +36,7 @@ import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch'; import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; import { INotebookCellMatchNoModel, INotebookFileMatchNoModel, IRawClosedNotebookFileMatch, genericCellMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search'; -import { globMatchesResource } from 'vs/workbench/services/editor/common/editorResolverService'; +import { globMatchesResource, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { ILogService } from 'vs/platform/log/common/log'; export class ExtHostNotebookController implements ExtHostNotebookShape { @@ -163,7 +163,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { providerDisplayName: extension.displayName || extension.name, displayName: registration.displayName, filenamePattern: viewOptionsFilenamePattern, - exclusive: registration.exclusive || false + priority: registration.exclusive ? RegisteredEditorPriority.exclusive : undefined }; } diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 8f74a0a4b69..382fd39c30a 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -211,6 +211,9 @@ export class ExtHostNotebookDocument { }, save() { return that._save(); + }, + [Symbol.for('debug.description')]() { + return `NotebookDocument(${this.uri.toString()})`; } }; this._notebook = Object.freeze(apiObject); diff --git a/src/vs/workbench/api/common/extHostNotebookEditor.ts b/src/vs/workbench/api/common/extHostNotebookEditor.ts index 8472fc1006d..4aec54c2651 100644 --- a/src/vs/workbench/api/common/extHostNotebookEditor.ts +++ b/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -71,6 +71,9 @@ export class ExtHostNotebookEditor { get viewColumn() { return that._viewColumn; }, + [Symbol.for('debug.description')]() { + return `NotebookEditor(${this.notebook.uri.toString()})`; + } }; ExtHostNotebookEditor.apiEditorsToExtHost.set(this._editor, this); diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index c746b79ed35..46f25cb7dd2 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -58,19 +58,26 @@ function getIconResource(decorations?: vscode.SourceControlResourceThemableDecor } } -function getHistoryItemIconDto(historyItem: vscode.SourceControlHistoryItem): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined { - if (!historyItem.icon) { +function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined { + if (!icon) { return undefined; - } else if (URI.isUri(historyItem.icon)) { - return historyItem.icon; - } else if (ThemeIcon.isThemeIcon(historyItem.icon)) { - return historyItem.icon; + } else if (URI.isUri(icon)) { + return icon; + } else if (ThemeIcon.isThemeIcon(icon)) { + return icon; } else { - const icon = historyItem.icon as { light: URI; dark: URI }; - return { light: icon.light, dark: icon.dark }; + const iconDto = icon as { light: URI; dark: URI }; + return { light: iconDto.light, dark: iconDto.dark }; } } +function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto { + const icon = getHistoryItemIconDto(historyItem.icon); + const labels = historyItem.labels?.map(l => ({ title: l.title, icon: getHistoryItemIconDto(l.icon) })); + + return { ...historyItem, icon, labels }; +} + function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number { if (!a.iconPath && !b.iconPath) { return 0; @@ -969,7 +976,14 @@ export class ExtHostSCM implements ExtHostSCMShape { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; const historyItems = await historyProvider?.provideHistoryItems(historyItemGroupId, options, token); - return historyItems?.map(item => ({ ...item, icon: getHistoryItemIconDto(item) })) ?? undefined; + return historyItems?.map(item => toSCMHistoryItemDto(item)) ?? undefined; + } + + async $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + const historyItems = await historyProvider?.provideHistoryItems2(options, token); + + return historyItems?.map(item => toSCMHistoryItemDto(item)) ?? undefined; } async $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise { @@ -979,7 +993,7 @@ export class ExtHostSCM implements ExtHostSCMShape { } const historyItem = await historyProvider.provideHistoryItemSummary(historyItemId, historyItemParentId, token); - return historyItem ? { ...historyItem, icon: getHistoryItemIconDto(historyItem) } : undefined; + return historyItem ? toSCMHistoryItemDto(historyItem) : undefined; } async $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/common/extHostSearch.ts b/src/vs/workbench/api/common/extHostSearch.ts index c2e0b93f7b9..f7da534aab8 100644 --- a/src/vs/workbench/api/common/extHostSearch.ts +++ b/src/vs/workbench/api/common/extHostSearch.ts @@ -25,7 +25,7 @@ export interface IExtHostSearch extends ExtHostSearchShape { export const IExtHostSearch = createDecorator('IExtHostSearch'); -export class ExtHostSearch implements ExtHostSearchShape { +export class ExtHostSearch implements IExtHostSearch { protected readonly _proxy: MainThreadSearchShape = this.extHostRpc.getProxy(MainContext.MainThreadSearch); protected _handlePool: number = 0; @@ -124,7 +124,7 @@ export class ExtHostSearch implements ExtHostSearchShape { $provideTextSearchResults(handle: number, session: number, rawQuery: IRawTextQuery, token: vscode.CancellationToken): Promise { const provider = this._textSearchProvider.get(handle); if (!provider || !provider.provideTextSearchResults) { - throw new Error(`2 Unknown provider ${handle}`); + throw new Error(`Unknown Text Search Provider ${handle}`); } const query = reviveQuery(rawQuery); @@ -135,7 +135,7 @@ export class ExtHostSearch implements ExtHostSearchShape { $provideAITextSearchResults(handle: number, session: number, rawQuery: IRawAITextQuery, token: vscode.CancellationToken): Promise { const provider = this._aiTextSearchProvider.get(handle); if (!provider || !provider.provideAITextSearchResults) { - throw new Error(`1 Unknown provider ${handle}`); + throw new Error(`Unknown AI Text Search Provider ${handle}`); } const query = reviveQuery(rawQuery); diff --git a/src/vs/workbench/api/common/extHostSecrets.ts b/src/vs/workbench/api/common/extHostSecrets.ts index d1af02ed1a2..13fb3293a35 100644 --- a/src/vs/workbench/api/common/extHostSecrets.ts +++ b/src/vs/workbench/api/common/extHostSecrets.ts @@ -9,26 +9,30 @@ import type * as vscode from 'vscode'; import { ExtHostSecretState } from 'vs/workbench/api/common/extHostSecretState'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; export class ExtensionSecrets implements vscode.SecretStorage { protected readonly _id: string; readonly #secretState: ExtHostSecretState; - private _onDidChange = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; - + readonly onDidChange: Event; + readonly disposables = new DisposableStore(); constructor(extensionDescription: IExtensionDescription, secretState: ExtHostSecretState) { this._id = ExtensionIdentifier.toKey(extensionDescription.identifier); this.#secretState = secretState; - this.#secretState.onDidChangePassword(e => { - if (e.extensionId === this._id) { - this._onDidChange.fire({ key: e.key }); - } - }); + this.onDidChange = Event.map( + Event.filter(this.#secretState.onDidChangePassword, e => e.extensionId === this._id), + e => ({ key: e.key }), + this.disposables + ); + } + + dispose() { + this.disposables.dispose(); } get(key: string): Promise { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index b870c3ab033..9ce31e7147c 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -921,7 +921,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection { let collection = this._environmentVariableCollections.get(extension.identifier.value); if (!collection) { - collection = new UnifiedEnvironmentVariableCollection(); + collection = this._register(new UnifiedEnvironmentVariableCollection()); this._setEnvironmentVariableCollection(extension.identifier.value, collection); } return collection.getScopedEnvironmentVariableCollection(undefined); @@ -936,7 +936,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public $initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void { collections.forEach(entry => { const extensionIdentifier = entry[0]; - const collection = new UnifiedEnvironmentVariableCollection(entry[1]); + const collection = this._register(new UnifiedEnvironmentVariableCollection(entry[1])); this._setEnvironmentVariableCollection(extensionIdentifier, collection); }); } @@ -952,20 +952,20 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I private _setEnvironmentVariableCollection(extensionIdentifier: string, collection: UnifiedEnvironmentVariableCollection): void { this._environmentVariableCollections.set(extensionIdentifier, collection); - collection.onDidChangeCollection(() => { + this._register(collection.onDidChangeCollection(() => { // When any collection value changes send this immediately, this is done to ensure // following calls to createTerminal will be created with the new environment. It will // result in more noise by sending multiple updates when called but collections are // expected to be small. this._syncEnvironmentVariableCollection(extensionIdentifier, collection); - }); + })); } } /** * Unified environment variable collection carrying information for all scopes, for a specific extension. */ -class UnifiedEnvironmentVariableCollection { +class UnifiedEnvironmentVariableCollection extends Disposable { readonly map: Map = new Map(); private readonly scopedCollections: Map = new Map(); readonly descriptionMap: Map = new Map(); @@ -983,6 +983,7 @@ class UnifiedEnvironmentVariableCollection { constructor( serialized?: ISerializableEnvironmentVariableCollection ) { + super(); this.map = new Map(serialized); } @@ -992,7 +993,7 @@ class UnifiedEnvironmentVariableCollection { if (!scopedCollection) { scopedCollection = new ScopedEnvironmentVariableCollection(this, scope); this.scopedCollections.set(scopedCollectionKey, scopedCollection); - scopedCollection.onDidChangeCollection(() => this._onDidChangeCollection.fire()); + this._register(scopedCollection.onDidChangeCollection(() => this._onDidChangeCollection.fire())); } return scopedCollection; } diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 64458c0b585..1ef9c81d439 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -15,17 +15,18 @@ import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecy import { MarshalledId } from 'vs/base/common/marshallingIds'; import { isDefined } from 'vs/base/common/types'; import { generateUuid } from 'vs/base/common/uuid'; -import { IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostTestingShape, ILocationDto, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostTestItemCollection, TestItemImpl, TestItemRootImpl, toItemFromContext } from 'vs/workbench/api/common/extHostTestItem'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { TestRunProfileKind, TestRunRequest, FileCoverage } from 'vs/workbench/api/common/extHostTypes'; +import { FileCoverage, TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; -import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; +import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; @@ -35,7 +36,7 @@ interface ControllerInfo { controller: vscode.TestController; profiles: Map; collection: ExtHostTestItemCollection; - extension: Readonly; + extension: IExtensionDescription; activeProfiles: Set; } @@ -45,7 +46,14 @@ let followupCounter = 0; const testResultInternalIDs = new WeakMap(); +export const IExtHostTesting = createDecorator('IExtHostTesting'); +export interface IExtHostTesting extends ExtHostTesting { + readonly _serviceBrand: undefined; +} + export class ExtHostTesting extends Disposable implements ExtHostTestingShape { + declare readonly _serviceBrand: undefined; + private readonly resultsChangedEmitter = this._register(new Emitter()); protected readonly controllers = new Map(); private readonly proxy: MainThreadTestingShape; @@ -61,8 +69,8 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { constructor( @IExtHostRpcService rpc: IExtHostRpcService, @ILogService private readonly logService: ILogService, - private readonly commands: ExtHostCommands, - private readonly editors: ExtHostDocumentsAndEditors, + @IExtHostCommands private readonly commands: IExtHostCommands, + @IExtHostDocumentsAndEditors private readonly editors: IExtHostDocumentsAndEditors, ) { super(); this.proxy = rpc.getProxy(MainContext.MainThreadTesting); @@ -111,6 +119,8 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { }); } + //#region public API + /** * Implements vscode.test.registerTestProvider */ @@ -218,9 +228,9 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { await this.proxy.$runTests({ preserveFocus: req.preserveFocus ?? true, + group: profileGroupToBitset[profile.kind], targets: [{ testIds: req.include?.map(t => TestId.fromExtHostTestItem(t, controller.collection.root.id).toString()) ?? [controller.collection.root.id], - profileGroup: profileGroupToBitset[profile.kind], profileId: profile.profileId, controllerId: profile.controllerId, }], @@ -236,6 +246,9 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return { dispose: () => { this.followupProviders.delete(provider); } }; } + //#endregion + + //#region RPC methods /** * @inheritdoc */ @@ -250,8 +263,8 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { /** * @inheritdoc */ - async $getCoverageDetails(coverageId: string, token: CancellationToken): Promise { - const details = await this.runTracker.getCoverageDetails(coverageId, token); + async $getCoverageDetails(coverageId: string, testId: string | undefined, token: CancellationToken): Promise { + const details = await this.runTracker.getCoverageDetails(coverageId, testId, token); return details?.map(Convert.TestCoverage.fromDetails); } @@ -412,6 +425,30 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return this.commands.executeCommand(command.command, ...(command.arguments || [])); } + /** + * Cancels an ongoing test run. + */ + public $cancelExtensionTestRun(runId: string | undefined) { + if (runId === undefined) { + this.runTracker.cancelAllRuns(); + } else { + this.runTracker.cancelRunById(runId); + } + } + + //#endregion + + public getMetadataForRun(run: vscode.TestRun) { + for (const tracker of this.runTracker.trackers) { + const taskId = tracker.getTaskIdForRun(run); + if (taskId) { + return { taskId, runId: tracker.id }; + } + } + + return undefined; + } + private async runControllerTestRequest(req: ICallProfileRunHandler | ICallProfileRunHandler, isContinuous: boolean, token: CancellationToken): Promise { const lookup = this.controllers.get(req.controllerId); if (!lookup) { @@ -467,17 +504,6 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { } } } - - /** - * Cancels an ongoing test run. - */ - public $cancelExtensionTestRun(runId: string | undefined) { - if (runId === undefined) { - this.runTracker.cancelAllRuns(); - } else { - this.runTracker.cancelRunById(runId); - } - } } // Deadline after being requested by a user that a test run is forcibly cancelled. @@ -500,7 +526,7 @@ class TestRunTracker extends Disposable { private readonly cts: CancellationTokenSource; private readonly endEmitter = this._register(new Emitter()); private readonly onDidDispose: Event; - private readonly publishedCoverage = new Map(); + private readonly publishedCoverage = new Map(); /** * Fires when a test ends, and no more tests are left running. @@ -526,7 +552,7 @@ class TestRunTracker extends Disposable { private readonly proxy: MainThreadTestingShape, private readonly logService: ILogService, private readonly profile: vscode.TestRunProfile | undefined, - private readonly extension: IRelaxedExtensionDescription, + private readonly extension: IExtensionDescription, parentToken?: CancellationToken, ) { super(); @@ -543,6 +569,17 @@ class TestRunTracker extends Disposable { })); } + /** Gets the task ID from a test run object. */ + public getTaskIdForRun(run: vscode.TestRun) { + for (const [taskId, { run: r }] of this.tasks) { + if (r === run) { + return taskId; + } + } + + return undefined; + } + /** Requests cancellation of the run. On the second call, forces cancellation. */ public cancel() { if (this.state === TestRunTrackerState.Running) { @@ -554,19 +591,33 @@ class TestRunTracker extends Disposable { } /** Gets details for a previously-emitted coverage object. */ - public getCoverageDetails(id: string, token: CancellationToken) { + public async getCoverageDetails(id: string, testId: string | undefined, token: CancellationToken): Promise { const [, taskId] = TestId.fromString(id).path; /** runId, taskId, URI */ const coverage = this.publishedCoverage.get(id); if (!coverage) { return []; } + const { report, extIds } = coverage; const task = this.tasks.get(taskId); if (!task) { throw new Error('unreachable: run task was not found'); } - return this.profile?.loadDetailedCoverage?.(task.run, coverage, token) ?? []; + let testItem: vscode.TestItem | undefined; + if (testId && report instanceof FileCoverage) { + const index = extIds.indexOf(testId); + if (index === -1) { + return []; // ?? + } + testItem = report.fromTests[index]; + } + + const details = testItem + ? this.profile?.loadDetailedCoverageForTest?.(task.run, report, testItem, token) + : this.profile?.loadDetailedCoverage?.(task.run, report, token); + + return (await details) ?? []; } /** Creates the public test run interface to give to extensions. */ @@ -582,10 +633,6 @@ class TestRunTracker extends Disposable { return; } - if (!this.dto.isIncluded(test)) { - return; - } - this.ensureTestIsKnown(test); fn(test, ...args); }; @@ -610,7 +657,6 @@ class TestRunTracker extends Disposable { // one-off map used to associate test items with incrementing IDs in `addCoverage`. // There's no need to include their entire ID, we just want to make sure they're // stable and unique. Normal map is okay since TestRun lifetimes are limited. - const testItemCoverageId = new Map(); const run: vscode.TestRun = { isPersisted: this.dto.isPersisted, token: this.cts.token, @@ -621,28 +667,21 @@ class TestRunTracker extends Disposable { return; } - const testItem = coverage instanceof FileCoverage ? coverage.testItem : undefined; - let testItemIdPart: undefined | number; - if (testItem) { + const fromTests = coverage instanceof FileCoverage ? coverage.fromTests : []; + if (fromTests.length) { checkProposedApiEnabled(this.extension, 'attributableCoverage'); - if (!this.dto.isIncluded(testItem)) { - throw new Error('Attempted to `addCoverage` for a test item not included in the run'); - } - - this.ensureTestIsKnown(testItem); - testItemIdPart = testItemCoverageId.get(testItem); - if (testItemIdPart === undefined) { - testItemIdPart = testItemCoverageId.size; - testItemCoverageId.set(testItem, testItemIdPart); + for (const test of fromTests) { + this.ensureTestIsKnown(test); } } const uriStr = coverage.uri.toString(); - const id = new TestId(testItemIdPart !== undefined - ? [runId, taskId, uriStr, String(testItemIdPart)] - : [runId, taskId, uriStr], - ).toString(); - this.publishedCoverage.set(id, coverage); + const id = new TestId([runId, taskId, uriStr]).toString(); + // it's a lil funky, but it's possible for a test item's ID to change after + // it's been reported if it's rehomed under a different parent. Record its + // ID at the time when the coverage report is generated so we can reference + // it later if needeed. + this.publishedCoverage.set(id, { report: coverage, extIds: fromTests.map(t => TestId.fromExtHostTestItem(t, ctrlId).toString()) }); this.proxy.$appendCoverage(runId, taskId, Convert.TestCoverage.fromFile(ctrlId, id, coverage)); }, //#region state mutation @@ -673,11 +712,7 @@ class TestRunTracker extends Disposable { } if (test) { - if (this.dto.isIncluded(test)) { - this.ensureTestIsKnown(test); - } else { - test = undefined; - } + this.ensureTestIsKnown(test); } this.proxy.$appendOutputToRun( @@ -694,7 +729,6 @@ class TestRunTracker extends Disposable { } ended = true; - testItemCoverageId.clear(); this.proxy.$finishedTestRunTask(runId, taskId); if (!--this.running) { this.markEnded(); @@ -778,9 +812,9 @@ export class TestRunCoordinator { /** * Gets a coverage report for a given run and task ID. */ - public getCoverageDetails(id: string, token: vscode.CancellationToken) { + public getCoverageDetails(id: string, testId: string | undefined, token: vscode.CancellationToken) { const runId = TestId.root(id); - return this.trackedById.get(runId)?.getCoverageDetails(id, token) || []; + return this.trackedById.get(runId)?.getCoverageDetails(id, testId, token) || []; } /** @@ -802,7 +836,7 @@ export class TestRunCoordinator { * `$startedExtensionTestRun` is not invoked. The run must eventually * be cancelled manually. */ - public prepareForMainThreadTestRun(extension: IRelaxedExtensionDescription, req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile, token: CancellationToken) { + public prepareForMainThreadTestRun(extension: IExtensionDescription, req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile, token: CancellationToken) { return this.getTracker(req, dto, profile, extension, token); } @@ -825,7 +859,7 @@ export class TestRunCoordinator { /** * Implements the public `createTestRun` API. */ - public createTestRun(extension: IRelaxedExtensionDescription, controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { + public createTestRun(extension: IExtensionDescription, controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { const existing = this.tracked.get(request); if (existing) { return existing.createRun(name); @@ -854,7 +888,7 @@ export class TestRunCoordinator { return tracker.createRun(name); } - private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile | undefined, extension: IRelaxedExtensionDescription, token?: CancellationToken) { + private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile | undefined, extension: IExtensionDescription, token?: CancellationToken) { const tracker = new TestRunTracker(dto, this.proxy, this.logService, profile, extension, token); this.tracked.set(req, tracker); this.trackedById.set(tracker.id, tracker); @@ -875,15 +909,10 @@ const tryGetProfileFromTestRunReq = (request: vscode.TestRunRequest) => { }; export class TestRunDto { - private readonly includePrefix: string[]; - private readonly excludePrefix: string[]; - public static fromPublic(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, persist: boolean) { return new TestRunDto( controllerId, generateUuid(), - request.include?.map(t => TestId.fromExtHostTestItem(t, controllerId).toString()) ?? [controllerId], - request.exclude?.map(t => TestId.fromExtHostTestItem(t, controllerId).toString()) ?? [], persist, collection, ); @@ -893,8 +922,6 @@ export class TestRunDto { return new TestRunDto( request.controllerId, request.runId, - request.testIds, - request.excludeExtIds, true, collection, ); @@ -903,30 +930,9 @@ export class TestRunDto { constructor( public readonly controllerId: string, public readonly id: string, - include: string[], - exclude: string[], public readonly isPersisted: boolean, public readonly colllection: ExtHostTestItemCollection, ) { - this.includePrefix = include.map(id => id + TestIdPathParts.Delimiter); - this.excludePrefix = exclude.map(id => id + TestIdPathParts.Delimiter); - } - - public isIncluded(test: vscode.TestItem) { - const id = TestId.fromExtHostTestItem(test, this.controllerId).toString() + TestIdPathParts.Delimiter; - for (const prefix of this.excludePrefix) { - if (id === prefix || id.startsWith(prefix)) { - return false; - } - } - - for (const prefix of this.includePrefix) { - if (id === prefix || id.startsWith(prefix)) { - return true; - } - } - - return false; } } diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index 44ed8f3804f..73334e5ac89 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -566,6 +566,9 @@ export class ExtHostTextEditor { }, hide() { _proxy.$tryHideEditor(id); + }, + [Symbol.for('debug.description')]() { + return `TextEditor(${this.document.uri.toString()})`; } }); } diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index 7ab8d65df53..277422f9acb 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -5,6 +5,7 @@ import * as arrays from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; @@ -13,7 +14,7 @@ import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; -export class ExtHostEditors implements ExtHostEditorsShape { +export class ExtHostEditors extends Disposable implements ExtHostEditorsShape { private readonly _onDidChangeTextEditorSelection = new Emitter(); private readonly _onDidChangeTextEditorOptions = new Emitter(); @@ -35,11 +36,11 @@ export class ExtHostEditors implements ExtHostEditorsShape { mainContext: IMainContext, private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, ) { + super(); this._proxy = mainContext.getProxy(MainContext.MainThreadTextEditors); - - this._extHostDocumentsAndEditors.onDidChangeVisibleTextEditors(e => this._onDidChangeVisibleTextEditors.fire(e)); - this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(e => this._onDidChangeActiveTextEditor.fire(e)); + this._register(this._extHostDocumentsAndEditors.onDidChangeVisibleTextEditors(e => this._onDidChangeVisibleTextEditors.fire(e))); + this._register(this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(e => this._onDidChangeActiveTextEditor.fire(e))); } getActiveTextEditor(): vscode.TextEditor | undefined { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6525d0f2009..6fad560a437 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -53,6 +53,7 @@ import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/ed import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; import * as types from './extHostTypes'; +import { IToolData } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; export namespace Command { @@ -1330,7 +1331,9 @@ export namespace DocumentLink { // ignore } } - return new types.DocumentLink(Range.to(link.range), target); + const result = new types.DocumentLink(Range.to(link.range), target); + result.tooltip = link.tooltip; + return result; } } @@ -2063,8 +2066,8 @@ export namespace TestCoverage { statement: fromCoverageCount(coverage.statementCoverage), branch: coverage.branchCoverage && fromCoverageCount(coverage.branchCoverage), declaration: coverage.declarationCoverage && fromCoverageCount(coverage.declarationCoverage), - testId: coverage instanceof types.FileCoverage && coverage.testItem ? - TestId.fromExtHostTestItem(coverage.testItem, controllerId).toString() : undefined, + testIds: coverage instanceof types.FileCoverage && coverage.fromTests.length ? + coverage.fromTests.map(t => TestId.fromExtHostTestItem(t, controllerId).toString()) : undefined, }; } } @@ -2241,23 +2244,69 @@ export namespace ChatFollowup { } } +export namespace LanguageModelChatMessageRole { + export function to(role: chatProvider.ChatMessageRole): vscode.LanguageModelChatMessageRole { + switch (role) { + case chatProvider.ChatMessageRole.System: return types.LanguageModelChatMessageRole.System; + case chatProvider.ChatMessageRole.User: return types.LanguageModelChatMessageRole.User; + case chatProvider.ChatMessageRole.Assistant: return types.LanguageModelChatMessageRole.Assistant; + } + } + + export function from(role: vscode.LanguageModelChatMessageRole): chatProvider.ChatMessageRole { + switch (role) { + case types.LanguageModelChatMessageRole.System: return chatProvider.ChatMessageRole.System; + case types.LanguageModelChatMessageRole.User: return chatProvider.ChatMessageRole.User; + case types.LanguageModelChatMessageRole.Assistant: return chatProvider.ChatMessageRole.Assistant; + } + return chatProvider.ChatMessageRole.User; + } +} export namespace LanguageModelChatMessage { export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage { - switch (message.role) { - case chatProvider.ChatMessageRole.System: return new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.System, message.content); - case chatProvider.ChatMessageRole.User: return new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.User, message.content); - case chatProvider.ChatMessageRole.Assistant: return new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.Assistant, message.content); + let content: string = ''; + let content2: vscode.LanguageModelChatMessageFunctionResultPart | undefined; + if (message.content.type === 'text') { + content = message.content.value; + } else { + content2 = new types.LanguageModelFunctionResultPart(message.content.name, message.content.value, message.content.isError); } + const role = LanguageModelChatMessageRole.to(message.role); + const result = new types.LanguageModelChatMessage(role, content, message.name); + if (content2 !== undefined) { + result.content2 = content2; + } + return result; } export function from(message: vscode.LanguageModelChatMessage): chatProvider.IChatMessage { - switch (message.role as types.LanguageModelChatMessageRole) { - case types.LanguageModelChatMessageRole.System: return { role: chatProvider.ChatMessageRole.System, content: message.content }; - case types.LanguageModelChatMessageRole.User: return { role: chatProvider.ChatMessageRole.User, content: message.content }; - case types.LanguageModelChatMessageRole.Assistant: return { role: chatProvider.ChatMessageRole.Assistant, content: message.content }; + + const role = LanguageModelChatMessageRole.from(message.role); + const name = message.name; + + let content: chatProvider.IChatMessagePart; + + if (message.content2 instanceof types.LanguageModelFunctionResultPart) { + content = { + type: 'function_result', + name: message.content2.name, + value: message.content2.content, + isError: message.content2.isError + }; + } else { + content = { + type: 'text', + value: message.content + }; } + + return { + role, + name, + content + }; } } @@ -2508,6 +2557,8 @@ export namespace ChatResponsePart { return ChatResponseDetectedParticipantPart.from(part); } else if (part instanceof types.ChatResponseWarningPart) { return ChatResponseWarningPart.from(part); + } else if (part instanceof types.ChatResponseConfirmationPart) { + return ChatResponseConfirmationPart.from(part); } return { @@ -2543,7 +2594,7 @@ export namespace ChatResponsePart { } export namespace ChatAgentRequest { - export function to(request: IChatAgentRequest): vscode.ChatRequest { + export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined): vscode.ChatRequest { return { prompt: request.message, command: request.command, @@ -2552,7 +2603,8 @@ export namespace ChatAgentRequest { references: request.variables.variables.map(ChatAgentValueReference.to), location: ChatLocation.to(request.location), acceptedConfirmationData: request.acceptedConfirmationData, - rejectedConfirmationData: request.rejectedConfirmationData + rejectedConfirmationData: request.rejectedConfirmationData, + location2 }; } } @@ -2695,3 +2747,13 @@ export namespace DebugTreeItem { }; } } + +export namespace LanguageModelToolDescription { + export function to(item: IToolData): vscode.LanguageModelToolDescription { + return { + name: item.name, + description: item.description, + parametersSchema: item.parametersSchema, + }; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 189c90b8f63..18c2eefe79d 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -271,6 +271,10 @@ export class Position { toJSON(): any { return { line: this.line, character: this.character }; } + + [Symbol.for('debug.description')]() { + return `(${this.line}:${this.character})`; + } } @es5ClassCompat @@ -417,6 +421,10 @@ export class Range { toJSON(): any { return [this.start, this.end]; } + + [Symbol.for('debug.description')]() { + return getDebugDescriptionOfRange(this); + } } @es5ClassCompat @@ -483,6 +491,29 @@ export class Selection extends Range { anchor: this.anchor }; } + + + [Symbol.for('debug.description')]() { + return getDebugDescriptionOfSelection(this); + } +} + +export function getDebugDescriptionOfRange(range: vscode.Range): string { + return range.isEmpty + ? `[${range.start.line}:${range.start.character})` + : `[${range.start.line}:${range.start.character} -> ${range.end.line}:${range.end.character})`; +} + +export function getDebugDescriptionOfSelection(selection: vscode.Selection): string { + let rangeStr = getDebugDescriptionOfRange(selection); + if (!selection.isEmpty) { + if (selection.active.isEqual(selection.start)) { + rangeStr = `|${rangeStr}`; + } else { + rangeStr = `${rangeStr}|`; + } + } + return rangeStr; } const validateConnectionToken = (connectionToken: string) => { @@ -4119,7 +4150,7 @@ export class FileCoverage implements vscode.FileCoverage { public statementCoverage: vscode.TestCoverageCount, public branchCoverage?: vscode.TestCoverageCount, public declarationCoverage?: vscode.TestCoverageCount, - public testItem?: vscode.TestItem, + public fromTests: vscode.TestItem[] = [], ) { } } @@ -4449,16 +4480,45 @@ export enum ChatLocation { Editor = 4, } +export class ChatRequestEditorData implements vscode.ChatRequestEditorData { + constructor( + readonly document: vscode.TextDocument, + readonly selection: vscode.Selection, + readonly wholeRange: vscode.Range, + ) { } +} + +export class ChatRequestNotebookData implements vscode.ChatRequestNotebookData { + constructor( + readonly cell: vscode.TextDocument + ) { } +} + export enum LanguageModelChatMessageRole { User = 1, Assistant = 2, System = 3 } +export class LanguageModelFunctionResultPart implements vscode.LanguageModelChatMessageFunctionResultPart { + + name: string; + content: string; + isError: boolean; + + constructor(name: string, content: string, isError?: boolean) { + this.name = name; + this.content = content; + this.isError = isError ?? false; + } +} + export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage { - static User(content: string, name?: string): LanguageModelChatMessage { - return new LanguageModelChatMessage(LanguageModelChatMessageRole.User, content, name); + static User(content: string | LanguageModelFunctionResultPart, name?: string): LanguageModelChatMessage { + const value = new LanguageModelChatMessage(LanguageModelChatMessageRole.User, typeof content === 'string' ? content : '', name); + value.content2 = content; + return value; } static Assistant(content: string, name?: string): LanguageModelChatMessage { @@ -4467,15 +4527,36 @@ export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage role: vscode.LanguageModelChatMessageRole; content: string; + content2: string | vscode.LanguageModelChatMessageFunctionResultPart; name: string | undefined; constructor(role: vscode.LanguageModelChatMessageRole, content: string, name?: string) { this.role = role; this.content = content; + this.content2 = content; this.name = name; } } +export class LanguageModelFunctionUsePart implements vscode.LanguageModelChatResponseFunctionUsePart { + name: string; + parameters: any; + + constructor(name: string, parameters: any) { + this.name = name; + this.parameters = parameters; + } +} + +export class LanguageModelTextPart implements vscode.LanguageModelChatResponseTextPart { + value: string; + + constructor(value: string) { + this.value = value; + + } +} + /** * @deprecated */ diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 08b842e84fb..61c5b48357c 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -33,12 +33,14 @@ import { IRawFileMatch2, ITextSearchResult, resultIsMatch } from 'vs/workbench/s import * as vscode from 'vscode'; import { ExtHostWorkspaceShape, IRelativePatternDto, IWorkspaceData, MainContext, MainThreadMessageOptions, MainThreadMessageServiceShape, MainThreadWorkspaceShape } from './extHost.protocol'; import { revive } from 'vs/base/common/marshalling'; +import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; export interface IExtHostWorkspaceProvider { getWorkspaceFolder2(uri: vscode.Uri, resolveParent?: boolean): Promise; resolveWorkspaceFolder(uri: vscode.Uri): Promise; getWorkspaceFolders2(): Promise; resolveProxy(url: string): Promise; + lookupAuthorization(authInfo: AuthInfo): Promise; loadCertificates(): Promise; } @@ -132,7 +134,7 @@ class ExtHostWorkspaceImpl extends Workspace { constructor(id: string, private _name: string, folders: vscode.WorkspaceFolder[], transient: boolean, configuration: URI | null, private _isUntitled: boolean, ignorePathCasing: (key: URI) => boolean) { super(id, folders.map(f => new WorkspaceFolder(f)), transient, configuration, ignorePathCasing); - this._structure = TernarySearchTree.forUris(ignorePathCasing); + this._structure = TernarySearchTree.forUris(ignorePathCasing, () => true); // setup the workspace folder data structure folders.forEach(folder => { @@ -626,6 +628,10 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac return this._proxy.$resolveProxy(url); } + lookupAuthorization(authInfo: AuthInfo): Promise { + return this._proxy.$lookupAuthorization(authInfo); + } + loadCertificates(): Promise { return this._proxy.$loadCertificates(); } diff --git a/src/vs/workbench/api/common/extensionHostMain.ts b/src/vs/workbench/api/common/extensionHostMain.ts index 2bd275cbc21..50c47ba98a8 100644 --- a/src/vs/workbench/api/common/extensionHostMain.ts +++ b/src/vs/workbench/api/common/extensionHostMain.ts @@ -11,7 +11,7 @@ import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { RPCProtocol } from 'vs/workbench/services/extensions/common/rpcProtocol'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -195,7 +195,7 @@ export class ExtensionHostMain { private static _transform(initData: IExtensionHostInitData, rpcProtocol: RPCProtocol): IExtensionHostInitData { initData.extensions.allExtensions.forEach((ext) => { - (>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)); + (>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)); }); initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot)); const extDevLocs = initData.environment.extensionDevelopmentLocationURI; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index fd03fd9ea32..dc48354e372 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -27,6 +27,7 @@ import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/c import type * as vscode from 'vscode'; import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { IExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -44,8 +45,9 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider, @IExtHostCommands commands: IExtHostCommands, + @IExtHostTesting testing: IExtHostTesting, ) { - super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands, testing); } protected override createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { @@ -78,9 +80,9 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { if (!this._terminalDisposedListener) { // React on terminal disposed and check if that is the debug terminal #12956 - this._terminalDisposedListener = this._terminalService.onDidCloseTerminal(terminal => { + this._terminalDisposedListener = this._register(this._terminalService.onDidCloseTerminal(terminal => { this._integratedTerminalInstances.onTerminalClosed(terminal); - }); + })); } const configProvider = await this._configurationService.getConfigProvider(); diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index 99f5d7614e8..d10dbbb7d99 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -8,6 +8,7 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import * as pfs from 'vs/base/node/pfs'; import { ILogService } from 'vs/platform/log/common/log'; +import { IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostSearch, reviveQuery } from 'vs/workbench/api/common/extHostSearch'; @@ -29,24 +30,59 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { private _registeredEHSearchProvider = false; + private _numThreadsPromise: Promise | undefined; + private readonly _disposables = new DisposableStore(); + private isDisposed = false; + constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @IURITransformerService _uriTransformer: IURITransformerService, + @IExtHostConfiguration private readonly configurationService: IExtHostConfiguration, @ILogService _logService: ILogService, ) { super(extHostRpc, _uriTransformer, _logService); - + this.getNumThreads = this.getNumThreads.bind(this); + this.getNumThreadsCached = this.getNumThreadsCached.bind(this); + this.handleConfigurationChanged = this.handleConfigurationChanged.bind(this); const outputChannel = new OutputChannel('RipgrepSearchUD', this._logService); - this._disposables.add(this.registerTextSearchProvider(Schemas.vscodeUserData, new RipgrepSearchProvider(outputChannel))); + this._disposables.add(this.registerTextSearchProvider(Schemas.vscodeUserData, new RipgrepSearchProvider(outputChannel, this.getNumThreadsCached))); if (initData.remote.isRemote && initData.remote.authority) { this._registerEHSearchProviders(); } + + configurationService.getConfigProvider().then(provider => { + if (this.isDisposed) { + return; + } + this._disposables.add(provider.onDidChangeConfiguration(this.handleConfigurationChanged)); + }); + } + + private handleConfigurationChanged(event: vscode.ConfigurationChangeEvent) { + if (!event.affectsConfiguration('search')) { + return; + } + this._numThreadsPromise = undefined; + } + + async getNumThreads(): Promise { + const configProvider = await this.configurationService.getConfigProvider(); + const numThreads = configProvider.getConfiguration('search').get('ripgrep.maxThreads'); + return numThreads; + } + + async getNumThreadsCached(): Promise { + if (!this._numThreadsPromise) { + this._numThreadsPromise = this.getNumThreads(); + } + return this._numThreadsPromise; } dispose(): void { + this.isDisposed = true; this._disposables.dispose(); } @@ -61,8 +97,8 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { this._registeredEHSearchProvider = true; const outputChannel = new OutputChannel('RipgrepSearchEH', this._logService); - this._disposables.add(this.registerTextSearchProvider(Schemas.file, new RipgrepSearchProvider(outputChannel))); - this._disposables.add(this.registerInternalFileSearchProvider(Schemas.file, new SearchService('fileSearchProvider'))); + this._disposables.add(this.registerTextSearchProvider(Schemas.file, new RipgrepSearchProvider(outputChannel, this.getNumThreadsCached))); + this._disposables.add(this.registerInternalFileSearchProvider(Schemas.file, new SearchService('fileSearchProvider', this.getNumThreadsCached))); } private registerInternalFileSearchProvider(scheme: string, provider: SearchService): IDisposable { @@ -90,7 +126,7 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { return super.$provideFileSearchResults(handle, session, rawQuery, token); } - override doInternalFileSearchWithCustomCallback(rawQuery: IFileQuery, token: vscode.CancellationToken, handleFileMatch: (data: URI[]) => void): Promise { + override async doInternalFileSearchWithCustomCallback(rawQuery: IFileQuery, token: vscode.CancellationToken, handleFileMatch: (data: URI[]) => void): Promise { const onResult = (ev: ISerializedSearchProgressItem) => { if (isSerializedFileMatch(ev)) { ev = [ev]; @@ -109,8 +145,8 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { if (!this._internalFileSearchProvider) { throw new Error('No internal file search handler'); } - - return >this._internalFileSearchProvider.doFileSearch(rawQuery, onResult, token); + const numThreads = await this.getNumThreadsCached(); + return >this._internalFileSearchProvider.doFileSearch(rawQuery, numThreads, onResult, token); } private async doInternalFileSearch(handle: number, session: number, rawQuery: IFileQuery, token: vscode.CancellationToken): Promise { diff --git a/src/vs/workbench/api/node/extHostStoragePaths.ts b/src/vs/workbench/api/node/extHostStoragePaths.ts index 8348d9490b2..e259b3b0d6d 100644 --- a/src/vs/workbench/api/node/extHostStoragePaths.ts +++ b/src/vs/workbench/api/node/extHostStoragePaths.ts @@ -69,14 +69,14 @@ export class ExtensionStoragePaths extends CommonExtensionStoragePaths { async function mkdir(dir: string): Promise { try { - await Promises.stat(dir); + await fs.promises.stat(dir); return; } catch { // doesn't exist, that's OK } try { - await Promises.mkdir(dir, { recursive: true }); + await fs.promises.mkdir(dir, { recursive: true }); } catch { } } @@ -103,7 +103,7 @@ class Lock extends Disposable { this._timer.cancel(); } try { - await Promises.utimes(filename, new Date(), new Date()); + await fs.promises.utimes(filename, new Date(), new Date()); } catch (err) { logService.error(err); logService.info(`Lock '${filename}': Could not update mtime.`); @@ -174,7 +174,7 @@ interface ILockfileContents { async function readLockfileContents(logService: ILogService, filename: string): Promise { let contents: Buffer; try { - contents = await Promises.readFile(filename); + contents = await fs.promises.readFile(filename); } catch (err) { // cannot read the file logService.error(err); @@ -196,7 +196,7 @@ async function readLockfileContents(logService: ILogService, filename: string): async function readmtime(logService: ILogService, filename: string): Promise { let stats: fs.Stats; try { - stats = await Promises.stat(filename); + stats = await fs.promises.stat(filename); } catch (err) { // cannot read the file stats to check if it is stale or not logService.error(err); @@ -279,7 +279,7 @@ async function checkStaleAndTryAcquireLock(logService: ILogService, filename: st async function tryDeleteAndAcquireLock(logService: ILogService, filename: string): Promise { logService.info(`Lock '${filename}': Deleting a stale lock.`); try { - await Promises.unlink(filename); + await fs.promises.unlink(filename); } catch (err) { // cannot delete the file // maybe the file is already deleted diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 13498806104..54b83109489 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { exec } from 'child_process'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter } from 'vs/base/common/event'; @@ -243,8 +244,8 @@ export class NodeExtHostTunnelService extends ExtHostTunnelService { let tcp: string = ''; let tcp6: string = ''; try { - tcp = await pfs.Promises.readFile('/proc/net/tcp', 'utf8'); - tcp6 = await pfs.Promises.readFile('/proc/net/tcp6', 'utf8'); + tcp = await fs.promises.readFile('/proc/net/tcp', 'utf8'); + tcp6 = await fs.promises.readFile('/proc/net/tcp6', 'utf8'); } catch (e) { // File reading error. No additional handling needed. } @@ -265,10 +266,10 @@ export class NodeExtHostTunnelService extends ExtHostTunnelService { try { const pid: number = Number(childName); const childUri = resources.joinPath(URI.file('/proc'), childName); - const childStat = await pfs.Promises.stat(childUri.fsPath); + const childStat = await fs.promises.stat(childUri.fsPath); if (childStat.isDirectory() && !isNaN(pid)) { - const cwd = await pfs.Promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); - const cmd = await pfs.Promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); + const cwd = await fs.promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); + const cmd = await fs.promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); processes.push({ pid, cwd, cmd }); } } catch (e) { diff --git a/src/vs/workbench/api/node/extensionHostProcess.ts b/src/vs/workbench/api/node/extensionHostProcess.ts index 785db7edd43..80a60c1d4c1 100644 --- a/src/vs/workbench/api/node/extensionHostProcess.ts +++ b/src/vs/workbench/api/node/extensionHostProcess.ts @@ -3,29 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import minimist from 'minimist'; import * as nativeWatchdog from 'native-watchdog'; import * as net from 'net'; -import * as minimist from 'minimist'; -import * as performance from 'vs/base/common/performance'; -import type { MessagePortMain } from 'vs/base/parts/sandbox/node/electronTypes'; +import { ProcessTimeRunOnceScheduler } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; import { isCancellationError, isSigPipeError, onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { PersistentProtocol, ProtocolConstants, BufferedEmitter } from 'vs/base/parts/ipc/common/ipc.net'; -import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import product from 'vs/platform/product/common/product'; -import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, ExtensionHostExitCode, IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { ExtensionHostMain, IExitFn } from 'vs/workbench/api/common/extensionHostMain'; -import { VSBuffer } from 'vs/base/common/buffer'; +import * as performance from 'vs/base/common/performance'; import { IURITransformer } from 'vs/base/common/uriIpc'; -import { Promises } from 'vs/base/node/pfs'; import { realpath } from 'vs/base/node/extpath'; -import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; -import { ProcessTimeRunOnceScheduler } from 'vs/base/common/async'; +import { Promises } from 'vs/base/node/pfs'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { BufferedEmitter, PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import type { MessagePortMain } from 'vs/base/parts/sandbox/node/electronTypes'; import { boolean } from 'vs/editor/common/config/editorOptions'; +import product from 'vs/platform/product/common/product'; +import { ExtensionHostMain, IExitFn } from 'vs/workbench/api/common/extensionHostMain'; +import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; import { createURITransformer } from 'vs/workbench/api/node/uriTransformer'; import { ExtHostConnectionType, readExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; +import { ExtensionHostExitCode, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, IExtHostSocketMessage, IExtensionHostInitData, MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IDisposable } from 'vs/base/common/lifecycle'; import 'vs/workbench/api/common/extHost.common.services'; import 'vs/workbench/api/node/extHost.node.services'; @@ -251,12 +252,14 @@ async function createExtHostProtocol(): Promise { readonly onMessage: Event = this._onMessage.event; private _terminating: boolean; + private _protocolListener: IDisposable; constructor() { this._terminating = false; - protocol.onMessage((msg) => { + this._protocolListener = protocol.onMessage((msg) => { if (isMessageOfType(msg, MessageType.Terminate)) { this._terminating = true; + this._protocolListener.dispose(); onTerminate('received terminate message from renderer'); } else { this._onMessage.fire(msg); diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index 519924eec13..0df3f98c980 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -17,6 +17,7 @@ import { URI } from 'vs/base/common/uri'; import { ILogService, LogLevel as LogServiceLevel } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { LogLevel, createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting, ProxyAgentParams, createNetPatch, loadSystemCertificates } from '@vscode/proxy-agent'; +import { AuthInfo } from 'vs/platform/request/common/request'; const systemCertificatesV2Default = false; @@ -32,9 +33,10 @@ export function connectProxyResolver( const doUseHostProxy = typeof useHostProxy === 'boolean' ? useHostProxy : !initData.remote.isRemote; const params: ProxyAgentParams = { resolveProxy: url => extHostWorkspace.resolveProxy(url), - lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostLogService, mainThreadTelemetry, configProvider, {}, initData.remote.isRemote), + lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote), getProxyURL: () => configProvider.getConfiguration('http').get('proxy'), getProxySupport: () => configProvider.getConfiguration('http').get('proxySupport') || 'off', + getNoProxyConfig: () => configProvider.getConfiguration('http').get('noProxy') || [], addCertificatesV1: () => certSettingV1(configProvider), addCertificatesV2: () => certSettingV2(configProvider), log: extHostLogService, @@ -67,6 +69,11 @@ export function connectProxyResolver( certs.then(certs => extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loaded certificates from main process', certs.length)); promises.push(certs); } + // Using https.globalAgent because it is shared with proxy.test.ts and mutable. + if (initData.environment.extensionTestsLocationURI && (https.globalAgent as any).testCertificates?.length) { + extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading test certificates'); + promises.push(Promise.resolve((https.globalAgent as any).testCertificates as string[])); + } return (await Promise.all(promises)).flat(); }, env: process.env, @@ -77,11 +84,16 @@ export function connectProxyResolver( } function createPatchedModules(params: ProxyAgentParams, resolveProxy: ReturnType) { + + function mergeModules(module: any, patch: any) { + return Object.assign(module.default || module, patch); + } + return { - http: Object.assign(http, createHttpPatch(params, http, resolveProxy)), - https: Object.assign(https, createHttpPatch(params, https, resolveProxy)), - net: Object.assign(net, createNetPatch(params, net)), - tls: Object.assign(tls, createTlsPatch(params, tls)) + http: mergeModules(http, createHttpPatch(params, http, resolveProxy)), + https: mergeModules(https, createHttpPatch(params, https, resolveProxy)), + net: mergeModules(net, createNetPatch(params, net)), + tls: mergeModules(tls, createTlsPatch(params, tls)) }; } @@ -129,14 +141,16 @@ function configureModuleLoading(extensionService: ExtHostExtensionService, looku } async function lookupProxyAuthorization( + extHostWorkspace: IExtHostWorkspaceProvider, extHostLogService: ILogService, mainThreadTelemetry: MainThreadTelemetryShape, configProvider: ExtHostConfigProvider, proxyAuthenticateCache: Record, + basicAuthCache: Record, isRemote: boolean, proxyURL: string, proxyAuthenticate: string | string[] | undefined, - state: { kerberosRequested?: boolean } + state: { kerberosRequested?: boolean; basicAuthCacheUsed?: boolean; basicAuthAttempt?: number } ): Promise { const cached = proxyAuthenticateCache[proxyURL]; if (proxyAuthenticate) { @@ -161,6 +175,45 @@ async function lookupProxyAuthorization( extHostLogService.error('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err); } } + const basicAuthHeader = authenticate.find(a => /^Basic( |$)/i.test(a)); + if (basicAuthHeader) { + try { + const cachedAuth = basicAuthCache[proxyURL]; + if (cachedAuth) { + if (state.basicAuthCacheUsed) { + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication deleting cached credentials', `proxyURL:${proxyURL}`); + delete basicAuthCache[proxyURL]; + } else { + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication using cached credentials', `proxyURL:${proxyURL}`); + state.basicAuthCacheUsed = true; + return cachedAuth; + } + } + state.basicAuthAttempt = (state.basicAuthAttempt || 0) + 1; + const realm = / realm="([^"]+)"/i.exec(basicAuthHeader)?.[1]; + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication lookup', `proxyURL:${proxyURL}`, `realm:${realm}`); + const url = new URL(proxyURL); + const authInfo: AuthInfo = { + scheme: 'basic', + host: url.hostname, + port: Number(url.port), + realm: realm || '', + isProxy: true, + attempt: state.basicAuthAttempt, + }; + const credentials = await extHostWorkspace.lookupAuthorization(authInfo); + if (credentials) { + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication received credentials', `proxyURL:${proxyURL}`, `realm:${realm}`); + const auth = 'Basic ' + Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64'); + basicAuthCache[proxyURL] = auth; + return auth; + } else { + extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication received no credentials', `proxyURL:${proxyURL}`, `realm:${realm}`); + } + } catch (err) { + extHostLogService.error('ProxyResolver#lookupProxyAuthorization Basic authentication failed', err); + } + } return undefined; } diff --git a/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts b/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts index 35e7ff35360..d5db3ca4683 100644 --- a/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts +++ b/src/vs/workbench/api/test/browser/extHost.api.impl.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { originalFSPath } from 'vs/base/common/resources'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts index b49d05ed00f..da5e8bb9e6f 100644 --- a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts @@ -17,7 +17,7 @@ import 'vs/editor/contrib/suggest/browser/suggest'; import 'vs/editor/contrib/rename/browser/rename'; import 'vs/editor/contrib/inlayHints/browser/inlayHintsController'; -import * as assert from 'assert'; +import assert from 'assert'; import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; @@ -1275,6 +1275,22 @@ suite('ExtHostLanguageFeatureCommands', function () { }); + testApiCmd('DocumentLink[] vscode.executeLinkProvider returns lack tooltip #213970', async function () { + disposables.push(extHost.registerDocumentLinkProvider(nullExtensionDescription, defaultSelector, { + provideDocumentLinks(): any { + const link = new types.DocumentLink(new types.Range(0, 0, 0, 20), URI.parse('foo:bar')); + link.tooltip = 'Link Tooltip'; + return [link]; + } + })); + + await rpcProtocol.sync(); + + const links1 = await commands.executeCommand('vscode.executeLinkProvider', model.uri); + assert.strictEqual(links1.length, 1); + assert.strictEqual(links1[0].tooltip, 'Link Tooltip'); + }); + test('Color provider', function () { diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts index dd77886bbf0..de02ffff745 100644 --- a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts +++ b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; diff --git a/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts b/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts index 045f3d77cc1..be11d3a7353 100644 --- a/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts +++ b/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { MainContext, IWorkspaceEditDto, MainThreadBulkEditsShape, IWorkspaceTextEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/extHostCommands.test.ts b/src/vs/workbench/api/test/browser/extHostCommands.test.ts index 5697ffe78cf..5353ca9c069 100644 --- a/src/vs/workbench/api/test/browser/extHostCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostCommands.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { MainThreadCommandsShape } from 'vs/workbench/api/common/extHost.protocol'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; diff --git a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts index ef43b937a98..298299b3e56 100644 --- a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; diff --git a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts index 26b419f6e06..8dca84bc5c1 100644 --- a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts index 6c6de8c7e17..2c866daee54 100644 --- a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DiagnosticCollection, ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; import { Diagnostic, DiagnosticSeverity, Range, DiagnosticRelatedInformation, Location } from 'vs/workbench/api/common/extHostTypes'; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts index 2132482c3aa..f4795adebb4 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts index 3f60f5ef898..298a2811954 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { Position } from 'vs/workbench/api/common/extHostTypes'; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts index 632487d43c0..9f0a28ec02a 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; diff --git a/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts index 3f4255c49c8..eda97538749 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts index cedfaa5e426..75f2ccce2d7 100644 --- a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; diff --git a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts index ba5e260f4d7..953aff7ea0c 100644 --- a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ExtHostFileSystemEventService } from 'vs/workbench/api/common/extHostFileSystemEventService'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts index 585e3745c10..6b7de3ca716 100644 --- a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts index 465663c8288..a88e5a79bb9 100644 --- a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MainThreadMessageService } from 'vs/workbench/api/browser/mainThreadMessageService'; import { IDialogService, IPrompt, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions, INotificationSource, INotificationSourceFilter, NotificationsFilter } from 'vs/platform/notification/common/notification'; diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index 5a7ed7e4e70..49a2f011645 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as vscode from 'vscode'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; diff --git a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts index 5a7e6f434c2..a1341bad58c 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Barrier } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -344,4 +344,3 @@ suite('NotebookKernel', function () { assert.ok(found); }); }); - diff --git a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts index 97bfb308b9f..54878ddb0cd 100644 --- a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -64,7 +64,8 @@ suite('ExtHostTelemetry', function () { publisher: 'vscode', version: '1.0.0', engines: { vscode: '*' }, - extensionLocation: URI.parse('fake') + extensionLocation: URI.parse('fake'), + enabledApiProposals: undefined, }; const createExtHostTelemetry = () => { diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index b82376cd88d..c4515796d05 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -14,7 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { mock, mockObject, MockObject } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import * as editorRange from 'vs/editor/common/core/range'; -import { ExtensionIdentifier, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -23,7 +23,7 @@ import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostTesting, TestRunCoordinator, TestRunDto, TestRunProfileImpl } from 'vs/workbench/api/common/extHostTesting'; import { ExtHostTestItemCollection, TestItemImpl } from 'vs/workbench/api/common/extHostTestItem'; import * as convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { Location, Position, Range, TestMessage, TestResultState, TestRunProfileKind, TestRunRequest as TestRunRequestImpl, TestTag } from 'vs/workbench/api/common/extHostTypes'; +import { Location, Position, Range, TestMessage, TestRunProfileKind, TestRunRequest as TestRunRequestImpl, TestTag } from 'vs/workbench/api/common/extHostTypes'; import { AnyCallRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestDiffOpType, TestItemExpandState, TestMessageType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; @@ -637,7 +637,7 @@ suite('ExtHost Testing', () => { let req: TestRunRequest; let dto: TestRunDto; - const ext: IRelaxedExtensionDescription = {} as any; + const ext: IExtensionDescription = {} as any; teardown(() => { for (const { id } of c.trackers) { @@ -864,29 +864,6 @@ suite('ExtHost Testing', () => { assert.strictEqual(proxy.$appendTestMessagesInRun.called, false); }); - test('excludes tests outside tree or explicitly excluded', () => { - const task = c.createTestRun(ext, 'ctrlId', single, { - profile: configuration, - include: [single.root.children.get('id-a')!], - exclude: [single.root.children.get('id-a')!.children.get('id-aa')!], - preserveFocus: false, - }, 'hello world', false); - - task.passed(single.root.children.get('id-a')!.children.get('id-aa')!); - task.passed(single.root.children.get('id-a')!.children.get('id-ab')!); - - assert.deepStrictEqual(proxy.$updateTestStateInRun.args.length, 1); - const args = proxy.$updateTestStateInRun.args[0]; - assert.deepStrictEqual(proxy.$updateTestStateInRun.args, [[ - args[0], - args[1], - new TestId(['ctrlId', 'id-a', 'id-ab']).toString(), - TestResultState.Passed, - undefined, - ]]); - task.end(); - }); - test('sets state of test with identical local IDs (#131827)', () => { const testA = single.root.children.get('id-a'); const testB = single.root.children.get('id-b'); diff --git a/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts b/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts index 106a30df4f8..98bc7766151 100644 --- a/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Lazy } from 'vs/base/common/lazy'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; diff --git a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts index 3f780a03299..1e442c1ed88 100644 --- a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { Emitter } from 'vs/base/common/event'; import { ExtHostTreeViews } from 'vs/workbench/api/common/extHostTreeViews'; diff --git a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts index ab38b202b6e..40a148042ff 100644 --- a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { MarkdownString, NotebookCellOutputItem, NotebookData, LanguageSelector, WorkspaceEdit } from 'vs/workbench/api/common/extHostTypeConverters'; import { isEmptyObject } from 'vs/base/common/types'; diff --git a/src/vs/workbench/api/test/browser/extHostTypes.test.ts b/src/vs/workbench/api/test/browser/extHostTypes.test.ts index 6cef861c9c8..3d1ff69b232 100644 --- a/src/vs/workbench/api/test/browser/extHostTypes.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypes.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/api/test/browser/extHostWebview.test.ts b/src/vs/workbench/api/test/browser/extHostWebview.test.ts index b741292bd00..e87300aeb05 100644 --- a/src/vs/workbench/api/test/browser/extHostWebview.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWebview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts index 6abca46d3ff..5b9dd312c74 100644 --- a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { basename } from 'vs/base/common/path'; import { URI, UriComponents } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts b/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts index 959705f233c..c6ef6939bfe 100644 --- a/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadBulkEdits.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IWorkspaceTextEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { mock } from 'vs/base/test/common/mock'; import { Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts b/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts index 0c5d47544d1..d7503ca0d50 100644 --- a/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadCommands.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MainThreadCommands } from 'vs/workbench/api/browser/mainThreadCommands'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; diff --git a/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts b/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts index a3a8e47755c..0c5a02f6606 100644 --- a/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { URI } from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts index bf67789ff10..2f89e98e358 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { URI, UriComponents } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts index 8eaa58798d7..949c2a78ffa 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { MainThreadDocumentContentProviders } from 'vs/workbench/api/browser/mainThreadDocumentContentProviders'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts index 8331e51cc03..fee65f21119 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { BoundModelReferenceCollection } from 'vs/workbench/api/browser/mainThreadDocuments'; import { timeout } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index d7190f1fe5c..f0d0538ce22 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/browser/mainThreadDocumentsAndEditors'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; diff --git a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts index 9809814a6c0..1ca165c1b5f 100644 --- a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore, IReference, ImmortalReference } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts b/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts index 8a714d8130d..dd20e3cd70c 100644 --- a/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { disposableTimeout, timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts index 57b58ab67f6..f4ead1836b2 100644 --- a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; diff --git a/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts b/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts index 5234d7abb34..c784f418950 100644 --- a/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadWorkspace.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts index 6f37db49441..bfd874acf2e 100644 --- a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts +++ b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { promiseWithResolvers, timeout } from 'vs/base/common/async'; +import { Mutable } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; import { ActivatedExtension, EmptyExtension, ExtensionActivationTimes, ExtensionsActivator, IExtensionsActivatorHost } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtensionDescriptionRegistry, IActivationEventsReader } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; @@ -14,6 +16,8 @@ import { ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbe suite('ExtensionsActivator', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + const idA = new ExtensionIdentifier(`a`); const idB = new ExtensionIdentifier(`b`); const idC = new ExtensionIdentifier(`c`); @@ -82,8 +86,8 @@ suite('ExtensionsActivator', () => { test('Supports having resolved extensions', async () => { const host = new SimpleExtensionsActivatorHost(); const bExt = desc(idB); - delete (bExt).main; - delete (bExt).browser; + delete (>bExt).main; + delete (>bExt).browser; const activator = createActivator(host, [ desc(idA, [idB]) ], [bExt]); @@ -100,7 +104,7 @@ suite('ExtensionsActivator', () => { [idB, extActivationB] ]); const bExt = desc(idB); - (bExt).api = 'none'; + (>bExt).api = 'none'; const activator = createActivator(host, [ desc(idA, [idB]) ], [bExt]); @@ -271,7 +275,8 @@ suite('ExtensionsActivator', () => { activationEvents, main: 'index.js', targetPlatform: TargetPlatform.UNDEFINED, - extensionDependencies: deps.map(d => d.value) + extensionDependencies: deps.map(d => d.value), + enabledApiProposals: undefined, }; } diff --git a/src/vs/workbench/api/test/common/extensionHostMain.test.ts b/src/vs/workbench/api/test/common/extensionHostMain.test.ts index 928c06a1b2c..1608511b527 100644 --- a/src/vs/workbench/api/test/common/extensionHostMain.test.ts +++ b/src/vs/workbench/api/test/common/extensionHostMain.test.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SerializedError, errorHandler, onUnexpectedError } from 'vs/base/common/errors'; import { isFirefox, isSafari } from 'vs/base/common/platform'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; @@ -48,7 +48,7 @@ suite('ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slo declare readonly _serviceBrand: undefined; getExtensionPathIndex() { return new class extends ExtensionPaths { - override findSubstr(key: URI): Readonly | undefined { + override findSubstr(key: URI): IExtensionDescription | undefined { findSubstrCount++; return nullExtensionDescription; } diff --git a/src/vs/workbench/api/test/node/extHostSearch.test.ts b/src/vs/workbench/api/test/node/extHostSearch.test.ts index 1502ef3f565..af20a5e455b 100644 --- a/src/vs/workbench/api/test/node/extHostSearch.test.ts +++ b/src/vs/workbench/api/test/node/extHostSearch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mapArrayOrNot } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -16,6 +16,7 @@ import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { MainContext, MainThreadSearchShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration.js'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { Range } from 'vs/workbench/api/common/extHostTypes'; import { URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; @@ -144,6 +145,26 @@ suite('ExtHostSearch', () => { rpcProtocol, new class extends mock() { override remote = { isRemote: false, authority: undefined, connectionData: null }; }, new URITransformerService(null), + new class extends mock() { + override async getConfigProvider(): Promise { + return { + onDidChangeConfiguration(_listener: (event: vscode.ConfigurationChangeEvent) => void) { }, + getConfiguration(): vscode.WorkspaceConfiguration { + return { + get() { }, + has() { + return false; + }, + inspect() { + return undefined; + }, + async update() { } + }; + }, + + } as ExtHostConfigProvider; + } + }, logService ); this._pfs = mockPFS as any; diff --git a/src/vs/workbench/api/test/node/extHostTunnelService.test.ts b/src/vs/workbench/api/test/node/extHostTunnelService.test.ts index 2f567ef7f4b..57fe15e52b4 100644 --- a/src/vs/workbench/api/test/node/extHostTunnelService.test.ts +++ b/src/vs/workbench/api/test/node/extHostTunnelService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { findPorts, getRootProcesses, getSockets, loadConnectionTable, loadListeningPorts, parseIpAddress, tryFindRootPorts } from 'vs/workbench/api/node/extHostTunnelService'; const tcp = diff --git a/src/vs/workbench/browser/actions.ts b/src/vs/workbench/browser/actions.ts index 1e166016ed3..dbe7355c8c2 100644 --- a/src/vs/workbench/browser/actions.ts +++ b/src/vs/workbench/browser/actions.ts @@ -96,9 +96,8 @@ export class CompositeMenuActions extends Disposable { const actions: IAction[] = []; if (this.contextMenuId) { - const menu = this.menuService.createMenu(this.contextMenuId, this.contextKeyService); - createAndFillInActionBarActions(menu, this.options, { primary: [], secondary: actions }); - menu.dispose(); + const menu = this.menuService.getMenuActions(this.contextMenuId, this.contextKeyService, this.options); + createAndFillInActionBarActions(menu, { primary: [], secondary: actions }); } return actions; diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 2c20a3572e2..7823fa988c0 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -63,7 +63,7 @@ class InspectContextKeysAction extends Action2 { const hoverFeedback = document.createElement('div'); const activeDocument = getActiveDocument(); activeDocument.body.appendChild(hoverFeedback); - disposables.add(toDisposable(() => activeDocument.body.removeChild(hoverFeedback))); + disposables.add(toDisposable(() => hoverFeedback.remove())); hoverFeedback.style.position = 'absolute'; hoverFeedback.style.pointerEvents = 'none'; diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index db6890b9017..188a94e1f51 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -725,7 +725,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const elementWithHover = getCustomHoverForElement(focusedElement as HTMLElement); if (elementWithHover) { - accessor.get(IHoverService).triggerUpdatableHover(elementWithHover as HTMLElement); + accessor.get(IHoverService).showManagedHover(elementWithHover as HTMLElement); } }, }); diff --git a/src/vs/workbench/browser/actions/textInputActions.ts b/src/vs/workbench/browser/actions/textInputActions.ts index dae9d15bd89..c06fb52ca30 100644 --- a/src/vs/workbench/browser/actions/textInputActions.ts +++ b/src/vs/workbench/browser/actions/textInputActions.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Disposable } from 'vs/base/common/lifecycle'; -import { EventHelper, addDisposableListener, getActiveDocument, getWindow } from 'vs/base/browser/dom'; +import { EventHelper, addDisposableListener, getActiveDocument, getWindow, isHTMLElement, isHTMLInputElement, isHTMLTextAreaElement } from 'vs/base/browser/dom'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { isNative } from 'vs/base/common/platform'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -54,8 +54,8 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo else { const clipboardText = await this.clipboardService.readText(); if ( - element instanceof HTMLTextAreaElement || - element instanceof HTMLInputElement + isHTMLTextAreaElement(element) || + isHTMLInputElement(element) ) { const selectionStart = element.selectionStart || 0; const selectionEnd = element.selectionEnd || 0; @@ -88,7 +88,7 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo } const target = e.target; - if (!(target instanceof HTMLElement) || (target.nodeName.toLowerCase() !== 'input' && target.nodeName.toLowerCase() !== 'textarea')) { + if (!(isHTMLElement(target)) || (target.nodeName.toLowerCase() !== 'input' && target.nodeName.toLowerCase() !== 'textarea')) { return; // only for inputs or textareas } diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 4020ac2b0e6..26c0b61d3d1 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -34,7 +34,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { isFolderBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/common/backup'; -import { getActiveElement, getActiveWindow } from 'vs/base/browser/dom'; +import { getActiveElement, getActiveWindow, isHTMLElement } from 'vs/base/browser/dom'; export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; @@ -404,7 +404,7 @@ class BlurAction extends Action2 { run(): void { const activeElement = getActiveElement(); - if (activeElement instanceof HTMLElement) { + if (isHTMLElement(activeElement)) { activeElement.blur(); } } diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index c22caa967b3..cffce2ce267 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -203,17 +203,16 @@ export class WorkbenchContextKeysHandler extends Disposable { private registerListeners(): void { this.editorGroupService.whenReady.then(() => { this.updateEditorAreaContextKeys(); - this.updateEditorGroupContextKeys(); + this.updateActiveEditorGroupContextKeys(); this.updateVisiblePanesContextKeys(); }); - this._register(this.editorService.onDidActiveEditorChange(() => this.updateEditorGroupContextKeys())); + this._register(this.editorService.onDidActiveEditorChange(() => this.updateActiveEditorGroupContextKeys())); this._register(this.editorService.onDidVisibleEditorsChange(() => this.updateVisiblePanesContextKeys())); - this._register(this.editorGroupService.onDidAddGroup(() => this.updateEditorGroupContextKeys())); - this._register(this.editorGroupService.onDidRemoveGroup(() => this.updateEditorGroupContextKeys())); - this._register(this.editorGroupService.onDidChangeGroupIndex(() => this.updateEditorGroupContextKeys())); - this._register(this.editorGroupService.onDidChangeActiveGroup(() => this.updateEditorGroupsContextKeys())); - this._register(this.editorGroupService.onDidChangeGroupLocked(() => this.updateEditorGroupsContextKeys())); + this._register(this.editorGroupService.onDidAddGroup(() => this.updateEditorGroupsContextKeys())); + this._register(this.editorGroupService.onDidRemoveGroup(() => this.updateEditorGroupsContextKeys())); + this._register(this.editorGroupService.onDidChangeGroupIndex(() => this.updateActiveEditorGroupContextKeys())); + this._register(this.editorGroupService.onDidChangeGroupLocked(() => this.updateActiveEditorGroupContextKeys())); this._register(this.editorGroupService.onDidChangeEditorPartOptions(() => this.updateEditorAreaContextKeys())); @@ -266,15 +265,22 @@ export class WorkbenchContextKeysHandler extends Disposable { } } - private updateEditorGroupContextKeys(): void { + // Context keys depending on the state of the editor group itself + private updateActiveEditorGroupContextKeys(): void { if (!this.editorService.activeEditor) { this.activeEditorGroupEmpty.set(true); } else { this.activeEditorGroupEmpty.reset(); } + + const activeGroup = this.editorGroupService.activeGroup; + this.activeEditorGroupIndex.set(activeGroup.index + 1); // not zero-indexed + this.activeEditorGroupLocked.set(activeGroup.isLocked); + this.updateEditorGroupsContextKeys(); } + // Context keys depending on the state of other editor groups private updateEditorGroupsContextKeys(): void { const groupCount = this.editorGroupService.count; if (groupCount > 1) { @@ -284,9 +290,7 @@ export class WorkbenchContextKeysHandler extends Disposable { } const activeGroup = this.editorGroupService.activeGroup; - this.activeEditorGroupIndex.set(activeGroup.index + 1); // not zero-indexed this.activeEditorGroupLast.set(activeGroup.index === groupCount - 1); - this.activeEditorGroupLocked.set(activeGroup.isLocked); } private updateEditorAreaContextKeys(): void { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 4e2809e149c..b8232cb8490 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -14,7 +14,7 @@ import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { Position, Parts, PanelOpensMaximizedOptions, IWorkbenchLayoutService, positionFromString, positionToString, panelOpensMaximizedFromString, PanelAlignment, ActivityBarPosition, LayoutSettings, MULTI_WINDOW_PARTS, SINGLE_WINDOW_PARTS, ZenModeSettings, EditorTabsMode, EditorActionsLocation, shouldShowCustomTitleBar } from 'vs/workbench/services/layout/browser/layoutService'; import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -208,11 +208,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private getContainerDimension(container: HTMLElement): IDimension { if (container === this.mainContainer) { - // main window - return this.mainContainerDimension; + return this.mainContainerDimension; // main window } else { - // auxiliary window - return getClientArea(container); + return getClientArea(container); // auxiliary window } } @@ -610,7 +608,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.stateModel.setRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN, false); } - this.stateModel.onDidChangeState(change => { + this._register(this.stateModel.onDidChangeState(change => { if (change.key === LayoutStateKeys.ACTIVITYBAR_HIDDEN) { this.setActivityBarHidden(change.value as boolean); } @@ -632,7 +630,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } this.doUpdateLayoutConfiguration(); - }); + })); // Layout Initialization State const initialEditorsState = this.getInitialEditorsState(); @@ -678,7 +676,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Only restore last viewlet if window was reloaded or we are in development mode let viewContainerToRestore: string | undefined; - if (!this.environmentService.isBuilt || lifecycleService.startupKind === StartupKind.ReloadedWindow) { + if ( + !this.environmentService.isBuilt || + lifecycleService.startupKind === StartupKind.ReloadedWindow || + this.environmentService.isExtensionDevelopment && !this.environmentService.extensionTestsLocationURI + ) { viewContainerToRestore = this.storageService.get(SidebarPart.activeViewletSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id); } else { viewContainerToRestore = this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id; @@ -1533,28 +1535,27 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi })); } - this._register(this.storageService.onWillSaveState(willSaveState => { - if (willSaveState.reason === WillSaveStateReason.SHUTDOWN) { - // Side Bar Size - const sideBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN) - ? this.workbenchGrid.getViewCachedVisibleSize(this.sideBarPartView) - : this.workbenchGrid.getViewSize(this.sideBarPartView).width; - this.stateModel.setInitializationValue(LayoutStateKeys.SIDEBAR_SIZE, sideBarSize as number); + this._register(this.storageService.onWillSaveState(e => { - // Panel Size - const panelSize = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) - ? this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) - : (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) === Position.BOTTOM ? this.workbenchGrid.getViewSize(this.panelPartView).height : this.workbenchGrid.getViewSize(this.panelPartView).width); - this.stateModel.setInitializationValue(LayoutStateKeys.PANEL_SIZE, panelSize as number); + // Side Bar Size + const sideBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN) + ? this.workbenchGrid.getViewCachedVisibleSize(this.sideBarPartView) + : this.workbenchGrid.getViewSize(this.sideBarPartView).width; + this.stateModel.setInitializationValue(LayoutStateKeys.SIDEBAR_SIZE, sideBarSize as number); - // Auxiliary Bar Size - const auxiliaryBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN) - ? this.workbenchGrid.getViewCachedVisibleSize(this.auxiliaryBarPartView) - : this.workbenchGrid.getViewSize(this.auxiliaryBarPartView).width; - this.stateModel.setInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE, auxiliaryBarSize as number); + // Panel Size + const panelSize = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) + ? this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) + : (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) === Position.BOTTOM ? this.workbenchGrid.getViewSize(this.panelPartView).height : this.workbenchGrid.getViewSize(this.panelPartView).width); + this.stateModel.setInitializationValue(LayoutStateKeys.PANEL_SIZE, panelSize as number); - this.stateModel.save(true, true); - } + // Auxiliary Bar Size + const auxiliaryBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN) + ? this.workbenchGrid.getViewCachedVisibleSize(this.auxiliaryBarPartView) + : this.workbenchGrid.getViewSize(this.auxiliaryBarPartView).width; + this.stateModel.setInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE, auxiliaryBarSize as number); + + this.stateModel.save(true, true); })); } @@ -1935,12 +1936,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi height: panelPosition === Position.BOTTOM ? this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT) : size.height }); } + this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_WAS_LAST_MAXIMIZED, !isMaximized); } - /** - * Returns whether or not the panel opens maximized - */ private panelOpensMaximized(): boolean { // The workbench grid currently prevents us from supporting panel maximization with non-center panel alignment @@ -2367,7 +2366,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi visible: !this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) }; - const middleSection: ISerializedNode[] = this.arrangeMiddleSectionNodes({ activityBar: activityBarNode, auxiliaryBar: auxiliaryBarNode, @@ -2717,6 +2715,7 @@ class LayoutStateModel extends Disposable { if (oldValue !== undefined) { return !oldValue; } + return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.DEFAULT; } diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 35f856b931a..6c9dbb9a0c9 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -87,6 +87,15 @@ body.web { text-decoration: none; } + +.monaco-workbench p > a { + text-decoration: var(--text-link-decoration); +} + +.monaco-workbench.underline-links { + --text-link-decoration: underline; +} + .monaco-workbench.hc-black p > a, .monaco-workbench.hc-light p > a { text-decoration: underline !important; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 4610ddff2c3..97f3861079f 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -363,10 +363,9 @@ export class ActivityBarCompositeBar extends PaneCompositeBar { } getActivityBarContextMenuActions(): IAction[] { - const activityBarPositionMenu = this.menuService.createMenu(MenuId.ActivityBarPositionMenu, this.contextKeyService); + const activityBarPositionMenu = this.menuService.getMenuActions(MenuId.ActivityBarPositionMenu, this.contextKeyService, { shouldForwardArgs: true, renderShortTitle: true }); const positionActions: IAction[] = []; - createAndFillInContextMenuActions(activityBarPositionMenu, { shouldForwardArgs: true, renderShortTitle: true }, { primary: [], secondary: positionActions }); - activityBarPositionMenu.dispose(); + createAndFillInContextMenuActions(activityBarPositionMenu, { primary: [], secondary: positionActions }); return [ new SubmenuAction('workbench.action.panel.position', localize('activity bar position', "Activity Bar Position"), positionActions), toAction({ id: ToggleSidebarPositionAction.ID, label: ToggleSidebarPositionAction.getLabel(this.layoutService), run: () => this.instantiationService.invokeFunction(accessor => new ToggleSidebarPositionAction().run(accessor)) }) diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index f42d3307fb7..b40341d217c 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -104,7 +104,8 @@ display: none; } -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.clicked:focus:before { +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.clicked:focus:before, +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.clicked:focus .active-item-indicator::before { border-left: none !important; /* no focus feedback when using mouse */ } diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index 16c40e836f2..3a2422d4c99 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -191,10 +191,9 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { actions.push(viewsSubmenuAction); } - const activityBarPositionMenu = this.menuService.createMenu(MenuId.ActivityBarPositionMenu, this.contextKeyService); + const activityBarPositionMenu = this.menuService.getMenuActions(MenuId.ActivityBarPositionMenu, this.contextKeyService, { shouldForwardArgs: true, renderShortTitle: true }); const positionActions: IAction[] = []; - createAndFillInContextMenuActions(activityBarPositionMenu, { shouldForwardArgs: true, renderShortTitle: true }, { primary: [], secondary: positionActions }); - activityBarPositionMenu.dispose(); + createAndFillInContextMenuActions(activityBarPositionMenu, { primary: [], secondary: positionActions }); actions.push(...[ new Separator(), diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index dda7c882d5e..e8957eae627 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/bannerpart'; import { localize2 } from 'vs/nls'; -import { $, addDisposableListener, append, asCSSUrl, clearNode, EventType } from 'vs/base/browser/dom'; +import { $, addDisposableListener, append, asCSSUrl, clearNode, EventType, isHTMLElement } from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -112,7 +112,7 @@ export class BannerPart extends Part implements IBannerService { if (this.focusedActionIndex < length) { const actionLink = this.messageActionsContainer?.children[this.focusedActionIndex]; - if (actionLink instanceof HTMLElement) { + if (isHTMLElement(actionLink)) { this.actionBar?.setFocusable(false); actionLink.focus(); } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index de10721d07c..24b1b97b7dd 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -319,11 +319,7 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { else if (badge instanceof NumberBadge) { if (badge.number) { let number = badge.number.toString(); - if (this.options.compact) { - if (badge.number > 99) { - number = ''; - } - } else if (badge.number > 999) { + if (badge.number > 999) { const noOfThousands = badge.number / 1000; const floor = Math.floor(noOfThousands); if (noOfThousands > floor) { @@ -332,6 +328,9 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { number = `${noOfThousands}K`; } } + if (this.options.compact && number.length >= 3) { + classes.push('compact-content'); + } this.badgeContent.textContent = number; show(this.badge); } diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index c86d7fd78f5..cbc4783066e 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -422,7 +422,7 @@ export abstract class CompositePart extends Part { const titleContainer = append(parent, $('.title-label')); const titleLabel = append(titleContainer, $('h2')); this.titleLabelElement = titleLabel; - const hover = this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), titleLabel, '')); + const hover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), titleLabel, '')); const $this = this; return { diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index e2fefb174e6..6bba2fcc4c5 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -34,7 +34,6 @@ import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { PixelRatio } from 'vs/base/browser/pixelRatio'; import { ILabelService } from 'vs/platform/label/common/label'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { IOutline } from 'vs/workbench/services/outline/browser/outline'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; @@ -85,7 +84,7 @@ class OutlineItem extends BreadcrumbsItem { } const template = renderer.renderTemplate(container); - renderer.renderElement(>{ + renderer.renderElement({ element, children: [], depth: 0, diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 138541a6ecf..066385368f8 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -242,6 +242,15 @@ export interface IEditorGroupTitleHeight { readonly offset: number; } +export interface IEditorGroupViewOptions { + + /** + * Whether the editor group should receive keyboard focus + * after creation or not. + */ + readonly preserveFocus?: boolean; +} + /** * A helper to access and mutate an editor group within an editor part. */ diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index b4665e6d8b2..eb9d1534e11 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -13,7 +13,7 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { GoFilter, IHistoryService } from 'vs/workbench/services/history/common/history'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, resolveCommandsContext, getCommandsContext, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID as NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID, resolveEditorsContext, getEditorsContext } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveCopyArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID, COPY_ACTIVE_EDITOR_COMMAND_ID, SPLIT_EDITOR, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID as NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorGroupsService, IEditorGroup, GroupsArrangement, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -34,9 +34,10 @@ import { IKeybindingRule, KeybindingWeight } from 'vs/platform/keybinding/common import { ILogService } from 'vs/platform/log/common/log'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { ActiveEditorAvailableEditorIdsContext, ActiveEditorContext, ActiveEditorGroupEmptyContext, AuxiliaryBarVisibleContext, EditorPartMaximizedEditorGroupContext, EditorPartMultipleEditorGroupsContext, IsAuxiliaryWindowFocusedContext, MultipleEditorGroupsContext, SideBarVisibleContext } from 'vs/workbench/common/contextkeys'; -import { URI } from 'vs/base/common/uri'; import { getActiveDocument } from 'vs/base/browser/dom'; import { ICommandActionTitle } from 'vs/platform/action/common/action'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { resolveCommandsContext } from 'vs/workbench/browser/parts/editor/editorCommandsContext'; class ExecuteCommandAction extends Action2 { @@ -61,12 +62,14 @@ abstract class AbstractSplitEditorAction extends Action2 { return preferredSideBySideGroupDirection(configurationService); } - override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { + override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { const editorGroupService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); - const commandContext = getCommandsContext(accessor, resourceOrContext, context); - splitEditor(editorGroupService, this.getDirection(configurationService), commandContext ? [commandContext] : undefined); + const direction = this.getDirection(configurationService); + const commandContext = resolveCommandsContext(accessor, args); + + splitEditor(editorGroupService, direction, commandContext); } } @@ -437,7 +440,7 @@ export class UnpinEditorAction extends Action { } } -export class CloseOneEditorAction extends Action { +export class CloseEditorTabAction extends Action { static readonly ID = 'workbench.action.closeActiveEditor'; static readonly LABEL = localize('closeOneEditor', "Close"); @@ -451,34 +454,29 @@ export class CloseOneEditorAction extends Action { } override async run(context?: IEditorCommandsContext): Promise { - let group: IEditorGroup | undefined; - let editorIndex: number | undefined; - if (context) { - group = this.editorGroupService.getGroup(context.groupId); - - if (group) { - editorIndex = context.editorIndex; // only allow editor at index if group is valid - } - } - + const group = context ? this.editorGroupService.getGroup(context.groupId) : this.editorGroupService.activeGroup; if (!group) { - group = this.editorGroupService.activeGroup; - } - - // Close specific editor in group - if (typeof editorIndex === 'number') { - const editorAtIndex = group.getEditorByIndex(editorIndex); - if (editorAtIndex) { - await group.closeEditor(editorAtIndex, { preserveFocus: context?.preserveFocus }); - return; - } - } - - // Otherwise close active editor in group - if (group.activeEditor) { - await group.closeEditor(group.activeEditor, { preserveFocus: context?.preserveFocus }); + // group mentioned in context does not exist return; } + + const targetEditor = context?.editorIndex !== undefined ? group.getEditorByIndex(context.editorIndex) : group.activeEditor; + if (!targetEditor) { + // No editor open or editor at index does not exist + return; + } + + const editors: EditorInput[] = []; + if (group.isSelected(targetEditor)) { + editors.push(...group.selectedEditors); + } else { + editors.push(targetEditor); + } + + // Close specific editors in group + for (const editor of editors) { + await group.closeEditor(editor, { preserveFocus: context?.preserveFocus }); + } } } @@ -569,6 +567,8 @@ abstract class AbstractCloseAllAction extends Action2 { override async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); + const logService = accessor.get(ILogService); + const progressService = accessor.get(IProgressService); const editorGroupService = accessor.get(IEditorGroupsService); const filesConfigurationService = accessor.get(IFilesConfigurationService); const fileDialogService = accessor.get(IFileDialogService); @@ -641,7 +641,7 @@ abstract class AbstractCloseAllAction extends Action2 { case ConfirmResult.CANCEL: return; case ConfirmResult.DONT_SAVE: - await editorService.revert(editors, { soft: true }); + await this.revertEditors(editorService, logService, progressService, editors); break; case ConfirmResult.SAVE: await editorService.save(editors, { reason: SaveReason.EXPLICIT }); @@ -661,7 +661,7 @@ abstract class AbstractCloseAllAction extends Action2 { case ConfirmResult.CANCEL: return; case ConfirmResult.DONT_SAVE: - await editorService.revert(editors, { soft: true }); + await this.revertEditors(editorService, logService, progressService, editors); break; case ConfirmResult.SAVE: await editorService.save(editors, { reason: SaveReason.EXPLICIT }); @@ -691,6 +691,33 @@ abstract class AbstractCloseAllAction extends Action2 { return this.doCloseAll(editorGroupService); } + private revertEditors(editorService: IEditorService, logService: ILogService, progressService: IProgressService, editors: IEditorIdentifier[]): Promise { + return progressService.withProgress({ + location: ProgressLocation.Window, // use window progress to not be too annoying about this operation + delay: 800, // delay so that it only appears when operation takes a long time + title: localize('reverting', "Reverting Editors..."), + }, () => this.doRevertEditors(editorService, logService, editors)); + } + + private async doRevertEditors(editorService: IEditorService, logService: ILogService, editors: IEditorIdentifier[]): Promise { + try { + // We first attempt to revert all editors with `soft: false`, to ensure that + // working copies revert to their state on disk. Even though we close editors, + // it is possible that other parties hold a reference to the working copy + // and expect it to be in a certain state after the editor is closed without + // saving. + await editorService.revert(editors); + } catch (error) { + logService.error(error); + + // if that fails, since we are about to close the editor, we accept that + // the editor cannot be reverted and instead do a soft revert that just + // enables us to close the editor. With this, a user can always close a + // dirty editor even when reverting fails. + await editorService.revert(editors, { soft: true }); + } + } + private async revealEditorsToConfirm(editors: ReadonlyArray, editorGroupService: IEditorGroupsService): Promise { try { const handledGroups = new Set(); @@ -1144,11 +1171,13 @@ export class ToggleMaximizeEditorGroupAction extends Action2 { }); } - override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { + override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { const editorGroupsService = accessor.get(IEditorGroupsService); - const { group } = resolveCommandsContext(editorGroupsService, getCommandsContext(accessor, resourceOrContext, context)); - editorGroupsService.toggleMaximizeGroup(group); + const resolvedContext = resolveCommandsContext(accessor, args); + if (resolvedContext.groupedEditors.length) { + editorGroupsService.toggleMaximizeGroup(resolvedContext.groupedEditors[0].group); + } } } @@ -2513,21 +2542,24 @@ abstract class BaseMoveCopyEditorToNewWindowAction extends Action2 { }); } - override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) { + override async run(accessor: ServicesAccessor, ...args: unknown[]) { const editorGroupService = accessor.get(IEditorGroupsService); - const editorsContext = resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context)); - if (editorsContext.length === 0) { + const resolvedContext = resolveCommandsContext(accessor, args); + if (!resolvedContext.groupedEditors.length) { return; } const auxiliaryEditorPart = await editorGroupService.createAuxiliaryEditorPart(); - const sourceGroup = editorsContext[0].group; // only single group supported for move/copy for now - const sourceEditors = editorsContext.filter(({ group }) => group === sourceGroup); + // only single group supported for move/copy for now + const { group, editors } = resolvedContext.groupedEditors[0]; + const options = { preserveFocus: resolvedContext.preserveFocus }; + const editorsWithOptions = editors.map(editor => ({ editor, options })); + if (this.move) { - sourceGroup.moveEditors(sourceEditors, auxiliaryEditorPart.activeGroup); + group.moveEditors(editorsWithOptions, auxiliaryEditorPart.activeGroup); } else { - sourceGroup.copyEditors(sourceEditors, auxiliaryEditorPart.activeGroup); + group.copyEditors(editorsWithOptions, auxiliaryEditorPart.activeGroup); } auxiliaryEditorPart.activeGroup.focus(); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index c8670de6fb0..7f137ca5244 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -3,13 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveElement } from 'vs/base/browser/dom'; -import { List } from 'vs/base/browser/ui/list/listWidget'; -import { coalesce, distinct } from 'vs/base/common/arrays'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Schemas, matchesScheme } from 'vs/base/common/network'; -import { extname, isEqual } from 'vs/base/common/resources'; +import { extname } from 'vs/base/common/resources'; import { isNumber, isObject, isString, isUndefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { isDiffEditor } from 'vs/editor/browser/editorBrowser'; @@ -23,7 +20,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { EditorResolution, IEditorOptions, IResourceEditorInput, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IListService, IOpenEvent } from 'vs/platform/list/browser/listService'; +import { IOpenEvent } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -31,17 +28,18 @@ import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/br import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext } from 'vs/workbench/common/contextkeys'; -import { CloseDirection, EditorInputCapabilities, EditorsOrder, IEditorCommandsContext, IEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorIdentifier, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; +import { CloseDirection, EditorInputCapabilities, EditorsOrder, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, IVisibleEditorPane, isEditorInputWithOptionsAndGroup } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { EditorGroupColumn, columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn'; -import { EditorGroupLayout, GroupDirection, GroupLocation, GroupsOrder, IEditorGroup, IEditorGroupsService, IEditorReplacement, isEditorGroup, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupsOrder, IEditorGroup, IEditorGroupsService, IEditorReplacement, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { DIFF_FOCUS_OTHER_SIDE, DIFF_FOCUS_PRIMARY_SIDE, DIFF_FOCUS_SECONDARY_SIDE, DIFF_OPEN_SIDE, registerDiffEditorCommands } from './diffEditorCommands'; +import { IResolvedEditorCommandsContext, resolveCommandsContext } from 'vs/workbench/browser/parts/editor/editorCommandsContext'; export const CLOSE_SAVED_EDITORS_COMMAND_ID = 'workbench.action.closeUnmodifiedEditors'; export const CLOSE_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeEditorsInGroup'; @@ -656,50 +654,25 @@ function registerFocusEditorGroupAtIndexCommands(): void { } } -export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, contexts?: IEditorCommandsContext[]): void { - let newGroup: IEditorGroup | undefined; - let sourceGroup: IEditorGroup | undefined; +export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, resolvedContext: IResolvedEditorCommandsContext): void { + if (!resolvedContext.groupedEditors.length) { + return; + } - for (const context of contexts ?? [undefined]) { - let currentGroup: IEditorGroup | undefined; - - if (context) { - currentGroup = editorGroupService.getGroup(context.groupId); - } else { - currentGroup = editorGroupService.activeGroup; - } - - if (!currentGroup) { - continue; - } - - if (!sourceGroup) { - sourceGroup = currentGroup; - } else if (sourceGroup.id !== currentGroup.id) { - continue; // Only support splitting from the same group - } - - // Add group - if (!newGroup) { - newGroup = editorGroupService.addGroup(currentGroup, direction); - } + // Only support splitting from one source group + const { group, editors } = resolvedContext.groupedEditors[0]; + const preserveFocus = resolvedContext.preserveFocus; + const newGroup = editorGroupService.addGroup(group, direction); + for (const editorToCopy of editors) { // Split editor (if it can be split) - let editorToCopy: EditorInput | undefined; - if (context && typeof context.editorIndex === 'number') { - editorToCopy = currentGroup.getEditorByIndex(context.editorIndex); - } else { - editorToCopy = currentGroup.activeEditor ?? undefined; - } - - // Copy the editor to the new group, else create an empty group if (editorToCopy && !editorToCopy.hasCapability(EditorInputCapabilities.Singleton)) { - currentGroup.copyEditor(editorToCopy, newGroup, { preserveFocus: context?.preserveFocus }); + group.copyEditor(editorToCopy, newGroup, { preserveFocus }); } } // Focus - newGroup?.focus(); + newGroup.focus(); } function registerSplitEditorCommands() { @@ -709,9 +682,9 @@ function registerSplitEditorCommands() { { id: SPLIT_EDITOR_LEFT, direction: GroupDirection.LEFT }, { id: SPLIT_EDITOR_RIGHT, direction: GroupDirection.RIGHT } ].forEach(({ id, direction }) => { - CommandsRegistry.registerCommand(id, function (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) { - const { editors } = getEditorsContext(accessor, resourceOrContext, context); - splitEditor(accessor.get(IEditorGroupsService), direction, editors); + CommandsRegistry.registerCommand(id, function (accessor, ...args) { + const resolvedContext = resolveCommandsContext(accessor, args); + splitEditor(accessor.get(IEditorGroupsService), direction, resolvedContext); }); }); } @@ -721,14 +694,14 @@ function registerCloseEditorCommands() { // A special handler for "Close Editor" depending on context // - keybindining: do not close sticky editors, rather open the next non-sticky editor // - menu: always close editor, even sticky ones - function closeEditorHandler(accessor: ServicesAccessor, forceCloseStickyEditors: boolean, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { + function closeEditorHandler(accessor: ServicesAccessor, forceCloseStickyEditors: boolean, ...args: unknown[]): Promise { const editorGroupsService = accessor.get(IEditorGroupsService); const editorService = accessor.get(IEditorService); let keepStickyEditors: boolean | undefined = undefined; if (forceCloseStickyEditors) { keepStickyEditors = false; // explicitly close sticky editors - } else if (resourceOrContext || context) { + } else if (args.length) { keepStickyEditors = false; // we have a context, as such this command was used e.g. from the tab context menu } else { keepStickyEditors = editorGroupsService.partOptions.preventPinnedEditorClose === 'keyboard' || editorGroupsService.partOptions.preventPinnedEditorClose === 'keyboardAndMouse'; // respect setting otherwise @@ -756,17 +729,12 @@ function registerCloseEditorCommands() { } // With context: proceed to close editors as instructed - const { editors, groups } = getEditorsContext(accessor, resourceOrContext, context); + const resolvedContext = resolveCommandsContext(accessor, args); + const preserveFocus = resolvedContext.preserveFocus; - return Promise.all(groups.map(async group => { - if (group) { - const editorsToClose = coalesce(editors - .filter(editor => editor.groupId === group.id) - .map(editor => typeof editor.editorIndex === 'number' ? group.getEditorByIndex(editor.editorIndex) : group.activeEditor)) - .filter(editor => !keepStickyEditors || !group.isSticky(editor)); - - await group.closeEditors(editorsToClose, { preserveFocus: context?.preserveFocus }); - } + return Promise.all(resolvedContext.groupedEditors.map(async ({ group, editors }) => { + const editorsToClose = editors.filter(editor => !keepStickyEditors || !group.isSticky(editor)); + await group.closeEditors(editorsToClose, { preserveFocus }); })); } @@ -776,13 +744,13 @@ function registerCloseEditorCommands() { when: undefined, primary: KeyMod.CtrlCmd | KeyCode.KeyW, win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KeyW] }, - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - return closeEditorHandler(accessor, false, resourceOrContext, context); + handler: (accessor, ...args: unknown[]) => { + return closeEditorHandler(accessor, false, ...args); } }); - CommandsRegistry.registerCommand(CLOSE_PINNED_EDITOR_COMMAND_ID, (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - return closeEditorHandler(accessor, true /* force close pinned editors */, resourceOrContext, context); + CommandsRegistry.registerCommand(CLOSE_PINNED_EDITOR_COMMAND_ID, (accessor, ...args: unknown[]) => { + return closeEditorHandler(accessor, true /* force close pinned editors */, ...args); }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -790,12 +758,10 @@ function registerCloseEditorCommands() { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyW), - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - return Promise.all(getEditorsContext(accessor, resourceOrContext, context).groups.map(async group => { - if (group) { - await group.closeAllEditors({ excludeSticky: true }); - return; - } + handler: (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(accessor, args); + return Promise.all(resolvedContext.groupedEditors.map(async ({ group }) => { + await group.closeAllEditors({ excludeSticky: true }); })); } }); @@ -806,19 +772,12 @@ function registerCloseEditorCommands() { when: ContextKeyExpr.and(ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext), primary: KeyMod.CtrlCmd | KeyCode.KeyW, win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KeyW] }, - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { + handler: (accessor, ...args: unknown[]) => { const editorGroupService = accessor.get(IEditorGroupsService); - const commandsContext = getCommandsContext(accessor, resourceOrContext, context); + const commandsContext = resolveCommandsContext(accessor, args); - let group: IEditorGroup | undefined; - if (commandsContext && typeof commandsContext.groupId === 'number') { - group = editorGroupService.getGroup(commandsContext.groupId); - } else { - group = editorGroupService.activeGroup; - } - - if (group) { - editorGroupService.removeGroup(group); + if (commandsContext.groupedEditors.length) { + editorGroupService.removeGroup(commandsContext.groupedEditors[0].group); } } }); @@ -828,11 +787,10 @@ function registerCloseEditorCommands() { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyU), - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - return Promise.all(getEditorsContext(accessor, resourceOrContext, context).groups.map(async group => { - if (group) { - await group.closeEditors({ savedOnly: true, excludeSticky: true }, { preserveFocus: context?.preserveFocus }); - } + handler: (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(accessor, args); + return Promise.all(resolvedContext.groupedEditors.map(async ({ group }) => { + await group.closeEditors({ savedOnly: true, excludeSticky: true }, { preserveFocus: resolvedContext.preserveFocus }); })); } }); @@ -843,24 +801,19 @@ function registerCloseEditorCommands() { when: undefined, primary: undefined, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyT }, - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const { editors, groups } = getEditorsContext(accessor, resourceOrContext, context); - return Promise.all(groups.map(async group => { - if (group) { - const editorsToKeep = editors - .filter(editor => editor.groupId === group.id) - .map(editor => typeof editor.editorIndex === 'number' ? group.getEditorByIndex(editor.editorIndex) : group.activeEditor); + handler: (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(accessor, args); - const editorsToClose = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).filter(editor => !editorsToKeep.includes(editor)); + return Promise.all(resolvedContext.groupedEditors.map(async ({ group, editors }) => { + const editorsToClose = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).filter(editor => !editors.includes(editor)); - for (const editorToKeep of editorsToKeep) { - if (editorToKeep) { - group.pinEditor(editorToKeep); - } + for (const editorToKeep of editors) { + if (editorToKeep) { + group.pinEditor(editorToKeep); } - - await group.closeEditors(editorsToClose, { preserveFocus: context?.preserveFocus }); } + + await group.closeEditors(editorsToClose, { preserveFocus: resolvedContext.preserveFocus }); })); } }); @@ -870,16 +823,15 @@ function registerCloseEditorCommands() { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); - if (group && editor) { + handler: async (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(accessor, args); + if (resolvedContext.groupedEditors.length) { + const { group, editors } = resolvedContext.groupedEditors[0]; if (group.activeEditor) { group.pinEditor(group.activeEditor); } - await group.closeEditors({ direction: CloseDirection.RIGHT, except: editor, excludeSticky: true }, { preserveFocus: context?.preserveFocus }); + await group.closeEditors({ direction: CloseDirection.RIGHT, except: editors[0], excludeSticky: true }, { preserveFocus: resolvedContext.preserveFocus }); } } }); @@ -889,62 +841,64 @@ function registerCloseEditorCommands() { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { + handler: async (accessor, ...args: unknown[]) => { const editorService = accessor.get(IEditorService); const editorResolverService = accessor.get(IEditorResolverService); const telemetryService = accessor.get(ITelemetryService); - const editorsAndGroup = resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context)); + const resolvedContext = resolveCommandsContext(accessor, args); const editorReplacements = new Map(); - for (const { editor, group } of editorsAndGroup) { - const untypedEditor = editor.toUntyped(); - if (!untypedEditor) { - return; // Resolver can only resolve untyped editors + for (const { group, editors } of resolvedContext.groupedEditors) { + for (const editor of editors) { + const untypedEditor = editor.toUntyped(); + if (!untypedEditor) { + return; // Resolver can only resolve untyped editors + } + + untypedEditor.options = { ...editorService.activeEditorPane?.options, override: EditorResolution.PICK }; + const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); + if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { + return; + } + + let editorReplacementsInGroup = editorReplacements.get(group); + if (!editorReplacementsInGroup) { + editorReplacementsInGroup = []; + editorReplacements.set(group, editorReplacementsInGroup); + } + + editorReplacementsInGroup.push({ + editor: editor, + replacement: resolvedEditor.editor, + forceReplaceDirty: editor.resource?.scheme === Schemas.untitled, + options: resolvedEditor.options + }); + + // Telemetry + type WorkbenchEditorReopenClassification = { + owner: 'rebornix'; + comment: 'Identify how a document is reopened'; + scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the resource' }; + ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the resource' }; + from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched from' }; + to: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched to' }; + }; + + type WorkbenchEditorReopenEvent = { + scheme: string; + ext: string; + from: string; + to: string; + }; + + telemetryService.publicLog2('workbenchEditorReopen', { + scheme: editor.resource?.scheme ?? '', + ext: editor.resource ? extname(editor.resource) : '', + from: editor.editorId ?? '', + to: resolvedEditor.editor.editorId ?? '' + }); } - - untypedEditor.options = { ...editorService.activeEditorPane?.options, override: EditorResolution.PICK }; - const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); - if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { - return; - } - - let editorReplacementsInGroup = editorReplacements.get(group); - if (!editorReplacementsInGroup) { - editorReplacementsInGroup = []; - editorReplacements.set(group, editorReplacementsInGroup); - } - - editorReplacementsInGroup.push({ - editor: editor, - replacement: resolvedEditor.editor, - forceReplaceDirty: editor.resource?.scheme === Schemas.untitled, - options: resolvedEditor.options - }); - - // Telemetry - type WorkbenchEditorReopenClassification = { - owner: 'rebornix'; - comment: 'Identify how a document is reopened'; - scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the resource' }; - ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the resource' }; - from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched from' }; - to: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched to' }; - }; - - type WorkbenchEditorReopenEvent = { - scheme: string; - ext: string; - from: string; - to: string; - }; - - telemetryService.publicLog2('workbenchEditorReopen', { - scheme: editor.resource?.scheme ?? '', - ext: editor.resource ? extname(editor.resource) : '', - from: editor.editorId ?? '', - to: resolvedEditor.editor.editorId ?? '' - }); } // Replace editor with resolved one and make active @@ -955,11 +909,12 @@ function registerCloseEditorCommands() { } }); - CommandsRegistry.registerCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, async (accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { + CommandsRegistry.registerCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, async (accessor: ServicesAccessor, ...args: unknown[]) => { const editorGroupService = accessor.get(IEditorGroupsService); - const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); - if (group) { + const resolvedContext = resolveCommandsContext(accessor, args); + if (resolvedContext.groupedEditors.length) { + const { group } = resolvedContext.groupedEditors[0]; await group.closeAllEditors(); if (group.count === 0 && editorGroupService.getGroup(group.id) /* could be gone by now */) { @@ -1002,11 +957,15 @@ function registerFocusEditorGroupWihoutWrapCommands(): void { function registerSplitEditorInGroupCommands(): void { - async function splitEditorInGroup(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - const editorGroupService = accessor.get(IEditorGroupsService); + async function splitEditorInGroup(accessor: ServicesAccessor, resolvedContext: IResolvedEditorCommandsContext): Promise { const instantiationService = accessor.get(IInstantiationService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); + if (!resolvedContext.groupedEditors.length) { + return; + } + + const { group, editors } = resolvedContext.groupedEditors[0]; + const editor = editors[0]; if (!editor) { return; } @@ -1033,15 +992,22 @@ function registerSplitEditorInGroupCommands(): void { } }); } - run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - return splitEditorInGroup(accessor, resourceOrContext, context); + run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + return splitEditorInGroup(accessor, resolveCommandsContext(accessor, args)); } }); - async function joinEditorInGroup(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - const editorGroupService = accessor.get(IEditorGroupsService); + async function joinEditorInGroup(resolvedContext: IResolvedEditorCommandsContext): Promise { + if (!resolvedContext.groupedEditors.length) { + return; + } + + const { group, editors } = resolvedContext.groupedEditors[0]; + const editor = editors[0]; + if (!editor) { + return; + } - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (!(editor instanceof SideBySideEditorInput)) { return; } @@ -1079,8 +1045,8 @@ function registerSplitEditorInGroupCommands(): void { } }); } - run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - return joinEditorInGroup(accessor, resourceOrContext, context); + run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + return joinEditorInGroup(resolveCommandsContext(accessor, args)); } }); @@ -1094,14 +1060,18 @@ function registerSplitEditorInGroupCommands(): void { f1: true }); } - async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - const editorGroupService = accessor.get(IEditorGroupsService); + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const resolvedContext = resolveCommandsContext(accessor, args); + if (!resolvedContext.groupedEditors.length) { + return; + } - const { editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); - if (editor instanceof SideBySideEditorInput) { - await joinEditorInGroup(accessor, resourceOrContext, context); - } else if (editor) { - await splitEditorInGroup(accessor, resourceOrContext, context); + const { editors } = resolvedContext.groupedEditors[0]; + + if (editors[0] instanceof SideBySideEditorInput) { + await joinEditorInGroup(resolvedContext); + } else if (editors[0]) { + await splitEditorInGroup(accessor, resolvedContext); } } }); @@ -1215,12 +1185,12 @@ function registerOtherEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.Enter), - handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); - if (group && editor) { - return group.pinEditor(editor); + handler: async (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(accessor, args); + for (const { group, editors } of resolvedContext.groupedEditors) { + for (const editor of editors) { + group.pinEditor(editor); + } } } }); @@ -1236,10 +1206,9 @@ function registerOtherEditorCommands(): void { } }); - function setEditorGroupLock(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext, locked?: boolean): void { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); + function setEditorGroupLock(accessor: ServicesAccessor, locked: boolean | undefined, ...args: unknown[]): void { + const resolvedContext = resolveCommandsContext(accessor, args); + const group = resolvedContext.groupedEditors[0]?.group; group?.lock(locked ?? !group.isLocked); } @@ -1252,8 +1221,8 @@ function registerOtherEditorCommands(): void { f1: true }); } - async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - setEditorGroupLock(accessor, resourceOrContext, context); + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + setEditorGroupLock(accessor, undefined, ...args); } }); @@ -1267,8 +1236,8 @@ function registerOtherEditorCommands(): void { f1: true }); } - async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - setEditorGroupLock(accessor, resourceOrContext, context, true); + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + setEditorGroupLock(accessor, true, ...args); } }); @@ -1282,8 +1251,8 @@ function registerOtherEditorCommands(): void { f1: true }); } - async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - setEditorGroupLock(accessor, resourceOrContext, context, false); + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + setEditorGroupLock(accessor, false, ...args); } }); @@ -1292,9 +1261,12 @@ function registerOtherEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: ActiveEditorStickyContext.toNegated(), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), - handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - for (const { editor, group } of resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context))) { - group.stickEditor(editor); + handler: async (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(accessor, args); + for (const { group, editors } of resolvedContext.groupedEditors) { + for (const editor of editors) { + group.stickEditor(editor); + } } } }); @@ -1331,9 +1303,12 @@ function registerOtherEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: ActiveEditorStickyContext, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), - handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - for (const { editor, group } of resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context))) { - group.unstickEditor(editor); + handler: async (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(accessor, args); + for (const { group, editors } of resolvedContext.groupedEditors) { + for (const editor of editors) { + group.unstickEditor(editor); + } } } }); @@ -1343,16 +1318,14 @@ function registerOtherEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { + handler: (accessor, ...args: unknown[]) => { const editorGroupService = accessor.get(IEditorGroupsService); const quickInputService = accessor.get(IQuickInputService); - const commandsContext = getCommandsContext(accessor, resourceOrContext, context); - if (commandsContext && typeof commandsContext.groupId === 'number') { - const group = editorGroupService.getGroup(commandsContext.groupId); - if (group) { - editorGroupService.activateGroup(group); // we need the group to be active - } + const commandsContext = resolveCommandsContext(accessor, args); + const group = commandsContext.groupedEditors[0]?.group; + if (group) { + editorGroupService.activateGroup(group); // we need the group to be active } return quickInputService.quickAccess.show(ActiveGroupEditorsByMostRecentlyUsedQuickAccess.PREFIX); @@ -1360,130 +1333,6 @@ function registerOtherEditorCommands(): void { }); } -type EditorsContext = { editors: IEditorCommandsContext[]; groups: Array }; -export function getEditorsContext(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): EditorsContext { - const editorGroupService = accessor.get(IEditorGroupsService); - const listService = accessor.get(IListService); - - const editorContext = getMultiSelectedEditorContexts(getCommandsContext(accessor, resourceOrContext, context), listService, editorGroupService); - - const activeGroup = editorGroupService.activeGroup; - if (editorContext.length === 0 && activeGroup.activeEditor) { - // add the active editor as fallback - editorContext.push({ - groupId: activeGroup.id, - editorIndex: activeGroup.getIndexOfEditor(activeGroup.activeEditor) - }); - } - - return { - editors: editorContext, - groups: distinct(editorContext.map(context => context.groupId)).map(groupId => editorGroupService.getGroup(groupId)) - }; -} - -export function resolveEditorsContext(context: EditorsContext): { editor: EditorInput; group: IEditorGroup }[] { - const { editors, groups } = context; - - const editorsAndGroup = editors.map(e => { - if (e.editorIndex === undefined) { - return undefined; - } - const group = groups.find(group => group && group.id === e.groupId); - const editor = group?.getEditorByIndex(e.editorIndex); - if (!editor || !group) { - return undefined; - } - return { editor, group }; - }); - - return coalesce(editorsAndGroup); -} - -export function getCommandsContext(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): IEditorCommandsContext | undefined { - const isUri = URI.isUri(resourceOrContext); - - const editorCommandsContext = isUri ? context : resourceOrContext ? resourceOrContext : context; - if (editorCommandsContext) { - return editorCommandsContext; - } - - if (isUri) { - const editorGroupService = accessor.get(IEditorGroupsService); - const editorGroup = editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).find(group => isEqual(group.activeEditor?.resource, resourceOrContext)); - if (editorGroup) { - return { groupId: editorGroup.index, editorIndex: editorGroup.getIndexOfEditor(editorGroup.activeEditor!) }; - } - } - - return undefined; -} - -export function resolveCommandsContext(editorGroupService: IEditorGroupsService, context?: IEditorCommandsContext): { group: IEditorGroup; editor?: EditorInput } { - - // Resolve from context - let group = context && typeof context.groupId === 'number' ? editorGroupService.getGroup(context.groupId) : undefined; - let editor = group && context && typeof context.editorIndex === 'number' ? group.getEditorByIndex(context.editorIndex) ?? undefined : undefined; - - // Fallback to active group as needed - if (!group) { - group = editorGroupService.activeGroup; - } - - // Fallback to active editor as needed - if (!editor) { - editor = group.activeEditor ?? undefined; - } - - return { group, editor }; -} - -export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsContext | undefined, listService: IListService, editorGroupService: IEditorGroupsService): IEditorCommandsContext[] { - - // First check for a focused list to return the selected items from - const list = listService.lastFocusedList; - if (list instanceof List && list.getHTMLElement() === getActiveElement()) { - const elementToContext = (element: IEditorIdentifier | IEditorGroup) => { - if (isEditorGroup(element)) { - return { groupId: element.id, editorIndex: undefined }; - } - - const group = editorGroupService.getGroup(element.groupId); - - return { groupId: element.groupId, editorIndex: group ? group.getIndexOfEditor(element.editor) : -1 }; - }; - - const onlyEditorGroupAndEditor = (e: IEditorIdentifier | IEditorGroup) => isEditorGroup(e) || isEditorIdentifier(e); - - const focusedElements: Array = list.getFocusedElements().filter(onlyEditorGroupAndEditor); - const focus = editorContext ? editorContext : focusedElements.length ? focusedElements.map(elementToContext)[0] : undefined; // need to take into account when editor context is { group: group } - - if (focus) { - const selection: Array = list.getSelectedElements().filter(onlyEditorGroupAndEditor); - - if (selection.length > 1) { - return selection.map(elementToContext); - } - - return [focus]; - } - } - // Check editors selected in the group (tabs) - else { - const group = editorContext ? editorGroupService.getGroup(editorContext.groupId) : editorGroupService.activeGroup; - const editor = editorContext && editorContext.editorIndex !== undefined ? group?.getEditorByIndex(editorContext.editorIndex) : group?.activeEditor; - // If the editor is selected, return all selected editors otherwise only use the editors context - if (group && editor) { - if (group.isSelected(editor)) { - return group.selectedEditors.map(se => ({ groupId: group.id, editorIndex: group.getIndexOfEditor(se) })); - } - } - } - - // Otherwise go with passed in context - return !!editorContext ? [editorContext] : []; -} - export function setup(): void { registerActiveEditorMoveCopyCommand(); registerEditorGroupsLayoutCommands(); diff --git a/src/vs/workbench/browser/parts/editor/editorCommandsContext.ts b/src/vs/workbench/browser/parts/editor/editorCommandsContext.ts new file mode 100644 index 00000000000..ee5957ddfa0 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/editorCommandsContext.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveElement } from 'vs/base/browser/dom'; +import { List } from 'vs/base/browser/ui/list/listWidget'; +import { URI } from 'vs/base/common/uri'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { IListService } from 'vs/platform/list/browser/listService'; +import { IEditorCommandsContext, isEditorCommandsContext, IEditorIdentifier, isEditorIdentifier } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IEditorGroup, IEditorGroupsService, isEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export interface IResolvedEditorCommandsContext { + readonly groupedEditors: { + readonly group: IEditorGroup; + readonly editors: EditorInput[]; + }[]; + readonly preserveFocus: boolean; +} + +export function resolveCommandsContext(accessor: ServicesAccessor, commandArgs: unknown[]): IResolvedEditorCommandsContext { + const editorGroupsService = accessor.get(IEditorGroupsService); + + const commandContext = getCommandsContext(accessor, commandArgs); + const preserveFocus = commandContext.length ? commandContext[0].preserveFocus || false : false; + const resolvedContext: IResolvedEditorCommandsContext = { groupedEditors: [], preserveFocus }; + + for (const editorContext of commandContext) { + const groupAndEditor = getEditorAndGroupFromContext(editorContext, editorGroupsService); + if (!groupAndEditor) { + continue; + } + + const { group, editor } = groupAndEditor; + + // Find group context if already added + let groupContext = undefined; + for (const targetGroupContext of resolvedContext.groupedEditors) { + if (targetGroupContext.group.id === group.id) { + groupContext = targetGroupContext; + break; + } + } + + // Otherwise add new group context + if (!groupContext) { + groupContext = { group, editors: [] }; + resolvedContext.groupedEditors.push(groupContext); + } + + // Add editor to group context + if (editor) { + groupContext.editors.push(editor); + } + } + + return resolvedContext; +} + +function getCommandsContext(accessor: ServicesAccessor, commandArgs: unknown[]): IEditorCommandsContext[] { + // Figure out if command is executed from a list + const listService = accessor.get(IListService); + const list = listService.lastFocusedList; + let isListAction = list instanceof List && list.getHTMLElement() === getActiveElement(); + + // Get editor context for which the command was triggered + let editorContext = getEditorContextFromCommandArgs(accessor, commandArgs, isListAction); + + // If the editor context can not be determind use the active editor + if (!editorContext) { + const editorGroupService = accessor.get(IEditorGroupsService); + const activeGroup = editorGroupService.activeGroup; + const activeEditor = activeGroup.activeEditor; + editorContext = { groupId: activeGroup.id, editorIndex: activeEditor ? activeGroup.getIndexOfEditor(activeEditor) : undefined }; + isListAction = false; + } + + const multiEditorContext = getMultiSelectContext(accessor, editorContext, isListAction); + + // Make sure the command context is the first one in the list + return moveCurrentEditorContextToFront(editorContext, multiEditorContext); +} + +function moveCurrentEditorContextToFront(editorContext: IEditorCommandsContext, multiEditorContext: IEditorCommandsContext[]): IEditorCommandsContext[] { + if (multiEditorContext.length <= 1) { + return multiEditorContext; + } + + const editorContextIndex = multiEditorContext.findIndex(context => + context.groupId === editorContext.groupId && + context.editorIndex === editorContext.editorIndex + ); + + if (editorContextIndex !== -1) { + multiEditorContext.splice(editorContextIndex, 1); + multiEditorContext.unshift(editorContext); + } else if (editorContext.editorIndex === undefined) { + multiEditorContext.unshift(editorContext); + } else { + throw new Error('Editor context not found in multi editor context'); + } + + return multiEditorContext; +} + +function getEditorContextFromCommandArgs(accessor: ServicesAccessor, commandArgs: unknown[], isListAcion: boolean): IEditorCommandsContext | undefined { + // We only know how to extraxt the command context from URI and IEditorCommandsContext arguments + const filteredArgs = commandArgs.filter(arg => isEditorCommandsContext(arg) || URI.isUri(arg)); + + // If the command arguments contain an editor context, use it + for (const arg of filteredArgs) { + if (isEditorCommandsContext(arg)) { + return arg; + } + } + + const editorService = accessor.get(IEditorService); + const editorGroupsService = accessor.get(IEditorGroupsService); + + // Otherwise, try to find the editor group by the URI of the resource + for (const uri of filteredArgs as URI[]) { + const editorIdentifiers = editorService.findEditors(uri); + if (editorIdentifiers.length) { + const editorIdentifier = editorIdentifiers[0]; + const group = editorGroupsService.getGroup(editorIdentifier.groupId); + return { groupId: editorIdentifier.groupId, editorIndex: group?.getIndexOfEditor(editorIdentifier.editor) }; + } + } + + const listService = accessor.get(IListService); + + // If there is no context in the arguments, try to find the context from the focused list + // if the action was executed from a list + if (isListAcion) { + const list = listService.lastFocusedList as List; + for (const focusedElement of list.getFocusedElements()) { + if (isGroupOrEditor(focusedElement)) { + return groupOrEditorToEditorContext(focusedElement, undefined, editorGroupsService); + } + } + } + + return undefined; +} + +function getMultiSelectContext(accessor: ServicesAccessor, editorContext: IEditorCommandsContext, isListAction: boolean): IEditorCommandsContext[] { + const listService = accessor.get(IListService); + const editorGroupsService = accessor.get(IEditorGroupsService); + + // If the action was executed from a list, return all selected editors + if (isListAction) { + const list = listService.lastFocusedList as List; + const selection = list.getSelectedElements().filter(isGroupOrEditor); + + if (selection.length > 1) { + return selection.map(e => groupOrEditorToEditorContext(e, editorContext.preserveFocus, editorGroupsService)); + } + } + // Check editors selected in the group (tabs) + else { + const group = editorGroupsService.getGroup(editorContext.groupId); + const editor = editorContext.editorIndex !== undefined ? group?.getEditorByIndex(editorContext.editorIndex) : group?.activeEditor; + // If the editor is selected, return all selected editors otherwise only use the editors context + if (group && editor && group.isSelected(editor)) { + return group.selectedEditors.map(editor => groupOrEditorToEditorContext({ editor, groupId: group.id }, editorContext.preserveFocus, editorGroupsService)); + } + } + + // Otherwise go with passed in context + return [editorContext]; +} + +function groupOrEditorToEditorContext(element: IEditorIdentifier | IEditorGroup, preserveFocus: boolean | undefined, editorGroupsService: IEditorGroupsService): IEditorCommandsContext { + if (isEditorGroup(element)) { + return { groupId: element.id, editorIndex: undefined, preserveFocus }; + } + + const group = editorGroupsService.getGroup(element.groupId); + + return { groupId: element.groupId, editorIndex: group ? group.getIndexOfEditor(element.editor) : -1, preserveFocus }; +} + +function isGroupOrEditor(element: unknown): element is IEditorIdentifier | IEditorGroup { + return isEditorGroup(element) || isEditorIdentifier(element); +} + +function getEditorAndGroupFromContext(commandContext: IEditorCommandsContext, editorGroupsService: IEditorGroupsService): { group: IEditorGroup; editor: EditorInput | undefined } | undefined { + const group = editorGroupsService.getGroup(commandContext.groupId); + if (!group) { + return undefined; + } + + if (commandContext.editorIndex === undefined) { + return { group, editor: undefined }; + } + + const editor = group.getEditorByIndex(commandContext.editorIndex); + return { group, editor }; +} diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 463b527e3b6..b11c66e6057 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -92,7 +92,7 @@ class DropOverlay extends Themable { this.groupView.element.appendChild(container); this.groupView.element.classList.add('dragged-over'); this._register(toDisposable(() => { - this.groupView.element.removeChild(container); + container.remove(); this.groupView.element.classList.remove('dragged-over'); })); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index d88cd673186..41e4e3ccc24 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/editorgroupview'; import { EditorGroupModel, IEditorOpenOptions, IGroupModelChangeEvent, ISerializedEditorGroupModel, isGroupEditorCloseEvent, isGroupEditorOpenEvent, isSerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; import { GroupIdentifier, CloseDirection, IEditorCloseEvent, IEditorPane, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, EditorResourceAccessor, EditorInputCapabilities, IUntypedEditorInput, DEFAULT_EDITOR_ASSOCIATION, SideBySideEditor, EditorCloseContext, IEditorWillMoveEvent, IEditorWillOpenEvent, IMatchEditorOptions, GroupModelChangeKind, IActiveEditorChangeEvent, IFindEditorOptions, IToolbarActions, TEXT_DIFF_EDITOR_ID } from 'vs/workbench/common/editor'; -import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorContext, ActiveEditorReadonlyContext, ActiveEditorCanRevertContext, ActiveEditorCanToggleReadonlyContext, ActiveCompareEditorCanSwapContext, MultipleEditorsSelectedInGroupContext, TwoEditorsSelectedInGroupContext } from 'vs/workbench/common/contextkeys'; +import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorContext, ActiveEditorReadonlyContext, ActiveEditorCanRevertContext, ActiveEditorCanToggleReadonlyContext, ActiveCompareEditorCanSwapContext, MultipleEditorsSelectedInGroupContext, TwoEditorsSelectedInGroupContext, SelectedEditorsInGroupFileOrUntitledResourceContextKey } from 'vs/workbench/common/contextkeys'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; @@ -28,7 +28,7 @@ import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DeferredPromise, Promises, RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; -import { IEditorGroupsView, IEditorGroupView, fillActiveEditorViewState, EditorServiceImpl, IEditorGroupTitleHeight, IInternalEditorOpenOptions, IInternalMoveCopyOptions, IInternalEditorCloseOptions, IInternalEditorTitleControlOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupView, fillActiveEditorViewState, EditorServiceImpl, IEditorGroupTitleHeight, IInternalEditorOpenOptions, IInternalMoveCopyOptions, IInternalEditorCloseOptions, IInternalEditorTitleControlOptions, IEditorPartsView, IEditorGroupViewOptions } from 'vs/workbench/browser/parts/editor/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAction, SubmenuAction } from 'vs/base/common/actions'; @@ -63,16 +63,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region factory - static createNew(editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, null, editorPartsView, groupsView, groupsLabel, groupIndex); + static createNew(editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService, options?: IEditorGroupViewOptions): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, null, editorPartsView, groupsView, groupsLabel, groupIndex, options); } - static createFromSerialized(serialized: ISerializedEditorGroupModel, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, serialized, editorPartsView, groupsView, groupsLabel, groupIndex); + static createFromSerialized(serialized: ISerializedEditorGroupModel, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService, options?: IEditorGroupViewOptions): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, serialized, editorPartsView, groupsView, groupsLabel, groupIndex, options); } - static createCopy(copyFrom: IEditorGroupView, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, copyFrom, editorPartsView, groupsView, groupsLabel, groupIndex); + static createCopy(copyFrom: IEditorGroupView, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService, options?: IEditorGroupViewOptions): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, copyFrom, editorPartsView, groupsView, groupsLabel, groupIndex, options); } //#endregion @@ -145,6 +145,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { readonly groupsView: IEditorGroupsView, private groupsLabel: string, private _index: number, + options: IEditorGroupViewOptions | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, @@ -236,7 +237,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#endregion // Restore editors if provided - const restoreEditorsPromise = this.restoreEditors(from) ?? Promise.resolve(); + const restoreEditorsPromise = this.restoreEditors(from, options) ?? Promise.resolve(); // Signal restored once editors have restored restoreEditorsPromise.finally(() => { @@ -258,6 +259,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const multipleEditorsSelectedContext = MultipleEditorsSelectedInGroupContext.bindTo(this.scopedContextKeyService); const twoEditorsSelectedContext = TwoEditorsSelectedInGroupContext.bindTo(this.scopedContextKeyService); + const selectedEditorsHaveFileOrUntitledResourceContext = SelectedEditorsInGroupFileOrUntitledResourceContextKey.bindTo(this.scopedContextKeyService); const groupActiveEditorContext = this.editorPartsView.bind(ActiveEditorContext, this); const groupActiveEditorIsReadonly = this.editorPartsView.bind(ActiveEditorReadonlyContext, this); @@ -354,6 +356,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { case GroupModelChangeKind.EDITORS_SELECTION: multipleEditorsSelectedContext.set(this.model.selectedEditors.length > 1); twoEditorsSelectedContext.set(this.model.selectedEditors.length === 2); + selectedEditorsHaveFileOrUntitledResourceContext.set(this.model.selectedEditors.every(e => e.resource && (this.fileService.hasProvider(e.resource) || e.resource.scheme === Schemas.untitled))); break; } @@ -534,7 +537,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleContainer.classList.toggle('show-file-icons', this.groupsView.partOptions.showIcons); } - private restoreEditors(from: IEditorGroupView | ISerializedEditorGroupModel | null): Promise | undefined { + private restoreEditors(from: IEditorGroupView | ISerializedEditorGroupModel | null, groupViewOptions?: IEditorGroupViewOptions): Promise | undefined { if (this.count === 0) { return; // nothing to show } @@ -557,24 +560,30 @@ export class EditorGroupView extends Themable implements IEditorGroupView { options.preserveFocus = true; // handle focus after editor is restored const internalOptions: IInternalEditorOpenOptions = { - preserveWindowOrder: true // handle window order after editor is restored + preserveWindowOrder: true, // handle window order after editor is restored + skipTitleUpdate: true, // update the title later for all editors at once }; const activeElement = getActiveElement(); // Show active editor (intentionally not using async to keep // `restoreEditors` from executing in same stack) - return this.doShowEditor(activeEditor, { active: true, isNew: false /* restored */ }, options, internalOptions).then(() => { + const result = this.doShowEditor(activeEditor, { active: true, isNew: false /* restored */ }, options, internalOptions).then(() => { // Set focused now if this is the active group and focus has // not changed meanwhile. This prevents focus from being // stolen accidentally on startup when the user already // clicked somewhere. - if (this.groupsView.activeGroup === this && activeElement && isActiveElement(activeElement)) { + if (this.groupsView.activeGroup === this && activeElement && isActiveElement(activeElement) && !groupViewOptions?.preserveFocus) { this.focus(); } }); + + // Restore editors in title control + this.titleControl.openEditors(this.editors); + + return result; } //#region event handling @@ -826,7 +835,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Ensure to show active editor if any if (this.model.activeEditor) { - this.titleControl.openEditor(this.model.activeEditor); + this.titleControl.openEditors(this.model.getEditors(EditorsOrder.SEQUENTIAL)); } } diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index 5d638871273..ef1b25dd681 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -466,7 +466,7 @@ export class EditorPanes extends Disposable { // Remove editor pane from parent const editorPaneContainer = this._activeEditorPane.getContainer(); if (editorPaneContainer) { - this.editorPanesParent.removeChild(editorPaneContainer); + editorPaneContainer.remove(); hide(editorPaneContainer); } diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index e467ab0270e..d785a513798 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -5,7 +5,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Part } from 'vs/workbench/browser/part'; -import { Dimension, $, EventHelper, addDisposableGenericMouseDownListener, getWindow, isAncestorOfActiveElement, getActiveElement } from 'vs/base/browser/dom'; +import { Dimension, $, EventHelper, addDisposableGenericMouseDownListener, getWindow, isAncestorOfActiveElement, getActiveElement, isHTMLElement } from 'vs/base/browser/dom'; import { Event, Emitter, Relay, PauseableEmitter } from 'vs/base/common/event'; import { contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { GroupDirection, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, GroupsOrder, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument, IEditorSideGroup, IEditorDropTargetDelegate, IEditorPart } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -14,7 +14,7 @@ import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGr import { GroupIdentifier, EditorInputWithOptions, IEditorPartOptions, IEditorPartOptionsChangeEvent, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme'; import { distinct, coalesce } from 'vs/base/common/arrays'; -import { IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions, IEditorPartsView, IEditorGroupsView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions, IEditorPartsView, IEditorGroupsView, IEditorGroupViewOptions } from 'vs/workbench/browser/parts/editor/editor'; import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -615,16 +615,16 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { } } - private doCreateGroupView(from?: IEditorGroupView | ISerializedEditorGroupModel | null): IEditorGroupView { + private doCreateGroupView(from?: IEditorGroupView | ISerializedEditorGroupModel | null, options?: IEditorGroupViewOptions): IEditorGroupView { // Create group view let groupView: IEditorGroupView; if (from instanceof EditorGroupView) { - groupView = EditorGroupView.createCopy(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService,); + groupView = EditorGroupView.createCopy(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService, options); } else if (isSerializedEditorGroupModel(from)) { - groupView = EditorGroupView.createFromSerialized(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService); + groupView = EditorGroupView.createFromSerialized(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService, options); } else { - groupView = EditorGroupView.createNew(this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService); + groupView = EditorGroupView.createNew(this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService, options); } // Keep in map @@ -934,7 +934,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { } createEditorDropTarget(container: unknown, delegate: IEditorDropTargetDelegate): IDisposable { - assertType(container instanceof HTMLElement); + assertType(isHTMLElement(container)); return this.scopedInstantiationService.createInstance(EditorDropTarget, container, delegate); } @@ -1192,7 +1192,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { return true; // success } - private doCreateGridControlWithState(serializedGrid: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[]): void { + private doCreateGridControlWithState(serializedGrid: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[], options?: IEditorGroupViewOptions): void { // Determine group views to reuse if any let reuseGroupViews: IEditorGroupView[]; @@ -1210,7 +1210,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { if (reuseGroupViews.length > 0) { groupView = reuseGroupViews.shift()!; } else { - groupView = this.doCreateGroupView(serializedEditorGroup); + groupView = this.doCreateGroupView(serializedEditorGroup, options); } groupViews.push(groupView); @@ -1342,15 +1342,15 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { }; } - applyState(state: IEditorPartUIState | 'empty'): Promise { + applyState(state: IEditorPartUIState | 'empty', options?: IEditorGroupViewOptions): Promise { if (state === 'empty') { return this.doApplyEmptyState(); } else { - return this.doApplyState(state); + return this.doApplyState(state, options); } } - private async doApplyState(state: IEditorPartUIState): Promise { + private async doApplyState(state: IEditorPartUIState, options?: IEditorGroupViewOptions): Promise { const groups = await this.doPrepareApplyState(); const resumeEvents = this.disposeGroups(true /* suspress events for the duration of applying state */); @@ -1359,7 +1359,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { // Grid Widget try { - this.doApplyGridState(state.serializedGrid, state.activeGroup); + this.doApplyGridState(state.serializedGrid, state.activeGroup, undefined, options); } finally { resumeEvents(); } @@ -1396,10 +1396,10 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { return groups; } - private doApplyGridState(gridState: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[]): void { + private doApplyGridState(gridState: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[], options?: IEditorGroupViewOptions): void { // Recreate grid widget from state - this.doCreateGridControlWithState(gridState, activeGroupId, editorGroupViewsToReuse); + this.doCreateGridControlWithState(gridState, activeGroupId, editorGroupViewsToReuse, options); // Layout this.doLayout(this._contentDimension); diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 574c97e4153..18123131e5b 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Emitter } from 'vs/base/common/event'; import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { GroupIdentifier } from 'vs/workbench/common/editor'; @@ -21,6 +21,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IAuxiliaryWindowOpenOptions, IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { generateUuid } from 'vs/base/common/uuid'; import { ContextKeyValue, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { isHTMLElement } from 'vs/base/browser/dom'; interface IEditorPartsUIState { readonly auxiliary: IAuxiliaryEditorPartState[]; @@ -161,7 +162,7 @@ export class EditorParts extends MultiWindowParts implements IEditor override getPart(element: HTMLElement): EditorPart; override getPart(groupOrElement: IEditorGroupView | GroupIdentifier | HTMLElement): EditorPart { if (this._parts.size > 1) { - if (groupOrElement instanceof HTMLElement) { + if (isHTMLElement(groupOrElement)) { const element = groupOrElement; return this.getPartByDocument(element.ownerDocument); @@ -375,7 +376,7 @@ export class EditorParts extends MultiWindowParts implements IEditor } } - async applyWorkingSet(workingSet: IEditorWorkingSet | 'empty'): Promise { + async applyWorkingSet(workingSet: IEditorWorkingSet | 'empty', options?: IEditorWorkingSetOptions): Promise { let workingSetState: IEditorWorkingSetState | 'empty' | undefined; if (workingSet === 'empty') { workingSetState = 'empty'; @@ -395,13 +396,15 @@ export class EditorParts extends MultiWindowParts implements IEditor if (!applied) { return false; } - await this.mainPart.applyState(workingSetState === 'empty' ? workingSetState : workingSetState.main); + await this.mainPart.applyState(workingSetState === 'empty' ? workingSetState : workingSetState.main, options); - // Restore Focus - const mostRecentActivePart = firstOrDefault(this.mostRecentActiveParts); - if (mostRecentActivePart) { - await mostRecentActivePart.whenReady; - mostRecentActivePart.activeGroup.focus(); + // Restore Focus unless instructed otherwise + if (!options?.preserveFocus) { + const mostRecentActivePart = firstOrDefault(this.mostRecentActiveParts); + if (mostRecentActivePart) { + await mostRecentActivePart.whenReady; + mostRecentActivePart.activeGroup.focus(); + } } return true; @@ -758,7 +761,7 @@ export class EditorParts extends MultiWindowParts implements IEditor let groupRegisteredContextKeys = this.registeredContextKeys.get(group.id); if (!groupRegisteredContextKeys) { groupRegisteredContextKeys = new Map(); - this.scopedContextKeys.set(group.id, groupRegisteredContextKeys); + this.registeredContextKeys.set(group.id, groupRegisteredContextKeys); } let scopedRegisteredContextKey = groupRegisteredContextKeys.get(provider.contextKey.key); diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index def52c5b61e..e434679f445 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -185,7 +185,7 @@ export class WorkspaceTrustRequiredPlaceholderEditor extends EditorPlaceholder { static readonly ID = 'workbench.editors.workspaceTrustRequiredEditor'; private static readonly LABEL = localize('trustRequiredEditor', "Workspace Trust Required"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredPlaceholderEditor, WorkspaceTrustRequiredPlaceholderEditor.ID, WorkspaceTrustRequiredPlaceholderEditor.LABEL); + static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredPlaceholderEditor, this.ID, this.LABEL); constructor( group: IEditorGroup, @@ -223,7 +223,7 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { private static readonly ID = 'workbench.editors.errorEditor'; private static readonly LABEL = localize('errorEditor', "Error Editor"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(ErrorPlaceholderEditor, ErrorPlaceholderEditor.ID, ErrorPlaceholderEditor.LABEL); + static readonly DESCRIPTOR = EditorPaneDescriptor.create(ErrorPlaceholderEditor, this.ID, this.LABEL); constructor( group: IEditorGroup, diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index c2ceeb2d7d7..845160784b1 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -1079,11 +1079,20 @@ export class ChangeLanguageAction extends Action2 { weight: KeybindingWeight.WorkbenchContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyM) }, - precondition: ContextKeyExpr.not('notebookEditorFocused') + precondition: ContextKeyExpr.not('notebookEditorFocused'), + metadata: { + description: localize('changeLanguageMode.description', "Change the language mode of the active text editor."), + args: [ + { + name: localize('changeLanguageMode.arg.name', "The name of the language mode to change to."), + constraint: (value: any) => typeof value === 'string', + } + ] + } }); } - override async run(accessor: ServicesAccessor): Promise { + override async run(accessor: ServicesAccessor, languageMode?: string): Promise { const quickInputService = accessor.get(IQuickInputService); const editorService = accessor.get(IEditorService); const languageService = accessor.get(ILanguageService); @@ -1162,7 +1171,7 @@ export class ChangeLanguageAction extends Action2 { }; picks.unshift(autoDetectLanguage); - const pick = await quickInputService.pick(picks, { placeHolder: localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); + const pick = typeof languageMode === 'string' ? { label: languageMode } : await quickInputService.pick(picks, { placeHolder: localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); if (!pick) { return; } @@ -1444,13 +1453,16 @@ export class ChangeEncodingAction extends Action2 { let guessedEncoding: string | undefined = undefined; if (fileService.hasProvider(resource)) { - const content = await textFileService.readStream(resource, { autoGuessEncoding: true }); + const content = await textFileService.readStream(resource, { + autoGuessEncoding: true, + candidateGuessEncodings: textResourceConfigurationService.getValue(resource, 'files.candidateGuessEncodings') + }); guessedEncoding = content.encoding; } const isReopenWithEncoding = (action === reopenWithEncodingPick); - const configuredEncoding = textResourceConfigurationService.getValue(resource ?? undefined, 'files.encoding'); + const configuredEncoding = textResourceConfigurationService.getValue(resource, 'files.encoding'); let directMatchIndex: number | undefined; let aliasMatchIndex: number | undefined; diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 395c83aa7a1..9007f132a8e 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -35,7 +35,7 @@ import { MergeGroupMode, IMergeGroupOptions } from 'vs/workbench/services/editor import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; -import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; +import { CloseEditorTabAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { assertAllDefined, assertIsDefined } from 'vs/base/common/types'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { basenameOrAuthority } from 'vs/base/common/resources'; @@ -111,7 +111,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { private tabsScrollbar: ScrollableElement | undefined; private tabSizingFixedDisposables: DisposableStore | undefined; - private readonly closeEditorAction = this._register(this.instantiationService.createInstance(CloseOneEditorAction, CloseOneEditorAction.ID, CloseOneEditorAction.LABEL)); + private readonly closeEditorAction = this._register(this.instantiationService.createInstance(CloseEditorTabAction, CloseEditorTabAction.ID, CloseEditorTabAction.LABEL)); private readonly unpinEditorAction = this._register(this.instantiationService.createInstance(UnpinEditorAction, UnpinEditorAction.ID, UnpinEditorAction.LABEL)); private readonly tabResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER)); @@ -1093,7 +1093,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } // Apply some datatransfer types to allow for dragging the element outside of the application - this.doFillResourceDataTransfers([editor], e, isNewWindowOperation); + this.doFillResourceDataTransfers(selectedEditors, e, isNewWindowOperation); scheduleAtNextAnimationFrame(getWindow(this.parent), () => this.updateDropFeedback(tab, false, e, tabIndex)); }, @@ -1288,24 +1288,24 @@ export class MultiEditorTabsControl extends EditorTabsControl { throw new BugIndicatingError(); } - const anchorIndex = this.groupView.getIndexOfEditor(anchor); - if (anchorIndex === -1) { + const anchorEditorIndex = this.groupView.getIndexOfEditor(anchor); + if (anchorEditorIndex === -1) { throw new BugIndicatingError(); } let selection = this.groupView.selectedEditors; // Unselect editors on other side of anchor in relation to the target - let currentIndex = anchorIndex; - while (currentIndex >= 0 && currentIndex <= this.groupView.count - 1) { - currentIndex = anchorIndex < editorIndex ? currentIndex - 1 : currentIndex + 1; + let currentEditorIndex = anchorEditorIndex; + while (currentEditorIndex >= 0 && currentEditorIndex <= this.groupView.count - 1) { + currentEditorIndex = anchorEditorIndex < editorIndex ? currentEditorIndex - 1 : currentEditorIndex + 1; - if (!this.tabsModel.isSelected(currentIndex)) { + const currentEditor = this.groupView.getEditorByIndex(currentEditorIndex); + if (!currentEditor) { break; } - const currentEditor = this.groupView.getEditorByIndex(currentIndex); - if (!currentEditor) { + if (!this.groupView.isSelected(currentEditor)) { break; } @@ -1313,12 +1313,12 @@ export class MultiEditorTabsControl extends EditorTabsControl { } // Select editors between anchor and target - const fromIndex = anchorIndex < editorIndex ? anchorIndex : editorIndex; - const toIndex = anchorIndex < editorIndex ? editorIndex : anchorIndex; + const fromEditorIndex = anchorEditorIndex < editorIndex ? anchorEditorIndex : editorIndex; + const toEditorIndex = anchorEditorIndex < editorIndex ? editorIndex : anchorEditorIndex; - const editorsToSelect = this.groupView.getEditors(EditorsOrder.SEQUENTIAL).slice(fromIndex, toIndex + 1); + const editorsToSelect = this.groupView.getEditors(EditorsOrder.SEQUENTIAL).slice(fromEditorIndex, toEditorIndex + 1); for (const editor of editorsToSelect) { - if (!this.tabsModel.isSelected(editor)) { + if (!this.groupView.isSelected(editor)) { selection.push(editor); } } @@ -1343,7 +1343,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const recentEditors = this.groupView.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); for (let i = 1; i < recentEditors.length; i++) { // First one is the active editor const recentEditor = recentEditors[i]; - if (this.tabsModel.isSelected(recentEditor)) { + if (this.groupView.isSelected(recentEditor)) { newActiveEditor = recentEditor; break; } diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 83823d3ec8f..69eb8b77262 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -61,12 +61,8 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } openEditor(editor: EditorInput, options: IInternalEditorOpenOptions): boolean { - const [editorTabController, otherTabController] = this.model.isSticky(editor) ? [this.stickyEditorTabsControl, this.unstickyEditorTabsControl] : [this.unstickyEditorTabsControl, this.stickyEditorTabsControl]; - const didChange = editorTabController.openEditor(editor, options); + const didChange = this.getEditorTabsController(editor).openEditor(editor, options); if (didChange) { - // HACK: To render all editor tabs on startup, otherwise only one row gets rendered - otherTabController.openEditors([]); - this.handleOpenedEditors(); } return didChange; diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index c3d7a0cce9d..2dab828b50a 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -160,7 +160,7 @@ export class SideBySideEditor extends AbstractEditorWithViewState { + if (e.event.removed) { + for (const removed of e.event.removed) { + this.removeAccount(e.providerId, removed.account); + } + } for (const changed of [...(e.event.changed ?? []), ...(e.event.added ?? [])]) { try { await this.addOrUpdateAccount(e.providerId, changed.account); @@ -344,11 +349,6 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction this.logService.error(e); } } - if (e.event.removed) { - for (const removed of e.event.removed) { - this.removeAccount(e.providerId, removed.account); - } - } })); } diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 52baa5324f7..174997a925d 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -237,6 +237,12 @@ text-align: center; } +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.compact-content .badge-content, +.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.compact-content .badge-content { + font-size: 8px; + padding: 0 3px; +} + .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact.progress-badge .badge-content::before { mask-size: 11px; diff --git a/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts index 066afd7adda..15a240f9150 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts @@ -92,6 +92,7 @@ export class NotificationAccessibleView implements IAccessibleViewImplentation { } return getProvider(); } + dispose() { } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 5d29de1726a..943136732c1 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -55,7 +55,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast // Count for the number of notifications over 800ms... interval: 800, // ...and ensure we are not showing more than MAX_NOTIFICATIONS - limit: NotificationsToasts.MAX_NOTIFICATIONS + limit: this.MAX_NOTIFICATIONS }; private readonly _onDidChangeVisibility = this._register(new Emitter()); @@ -602,7 +602,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast if (visible) { notificationsToastsContainer.appendChild(toast.container); } else { - notificationsToastsContainer.removeChild(toast.container); + toast.container.remove(); } // Update visibility in model diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 65ad1afe6cc..c1fda244291 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -30,7 +30,7 @@ import { defaultButtonStyles, defaultProgressBarStyles } from 'vs/platform/theme import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; export class NotificationsListDelegate implements IListVirtualDelegate { @@ -379,14 +379,14 @@ export class NotificationTemplateRenderer extends Disposable { this.renderSeverity(notification); // Message - const messageCustomHover = this.inputDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.template.message, '')); + const messageCustomHover = this.inputDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.template.message, '')); const messageOverflows = this.renderMessage(notification, messageCustomHover); // Secondary Actions this.renderSecondaryActions(notification, messageOverflows); // Source - const sourceCustomHover = this.inputDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.template.source, '')); + const sourceCustomHover = this.inputDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.template.source, '')); this.renderSource(notification, sourceCustomHover); // Buttons @@ -424,7 +424,7 @@ export class NotificationTemplateRenderer extends Disposable { this.template.icon.classList.add(...ThemeIcon.asClassNameArray(this.toSeverityIcon(notification.severity))); } - private renderMessage(notification: INotificationViewItem, customHover: IUpdatableHover): boolean { + private renderMessage(notification: INotificationViewItem, customHover: IManagedHover): boolean { clearNode(this.template.message); this.template.message.appendChild(NotificationMessageRenderer.render(notification.message, { callback: link => this.openerService.open(URI.parse(link), { allowCommands: true }), @@ -474,7 +474,7 @@ export class NotificationTemplateRenderer extends Disposable { actions.forEach(action => this.template.toolbar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) })); } - private renderSource(notification: INotificationViewItem, sourceCustomHover: IUpdatableHover): void { + private renderSource(notification: INotificationViewItem, sourceCustomHover: IManagedHover): void { if (notification.expanded && notification.source) { this.template.source.textContent = localize('notificationSource', "Source: {0}", notification.source); sourceCustomHover.update(notification.source); diff --git a/src/vs/workbench/browser/parts/paneCompositePart.ts b/src/vs/workbench/browser/parts/paneCompositePart.ts index 68e797d69e1..cf29cbed9f1 100644 --- a/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -629,8 +629,8 @@ export abstract class AbstractPaneCompositePart extends CompositePart true); + const menu = this.menuService.getMenuActions(ViewsSubMenu, scopedContextKeyService, { shouldForwardArgs: true, renderShortTitle: true }); + createAndFillInActionBarActions(menu, { primary: viewsActions, secondary: [] }, () => true); disposables.dispose(); return viewsActions.length > 1 && viewsActions.some(a => a.enabled) ? new SubmenuAction('views', localize('views', "Views"), viewsActions) : undefined; } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index e8e8b49bce5..845274fa940 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -149,14 +149,12 @@ export class PanelPart extends AbstractPaneCompositePart { } private fillExtraContextMenuActions(actions: IAction[]): void { - const panelPositionMenu = this.menuService.createMenu(MenuId.PanelPositionMenu, this.contextKeyService); - const panelAlignMenu = this.menuService.createMenu(MenuId.PanelAlignmentMenu, this.contextKeyService); + const panelPositionMenu = this.menuService.getMenuActions(MenuId.PanelPositionMenu, this.contextKeyService, { shouldForwardArgs: true }); + const panelAlignMenu = this.menuService.getMenuActions(MenuId.PanelAlignmentMenu, this.contextKeyService, { shouldForwardArgs: true }); const positionActions: IAction[] = []; const alignActions: IAction[] = []; - createAndFillInContextMenuActions(panelPositionMenu, { shouldForwardArgs: true }, { primary: [], secondary: positionActions }); - createAndFillInContextMenuActions(panelAlignMenu, { shouldForwardArgs: true }, { primary: [], secondary: alignActions }); - panelAlignMenu.dispose(); - panelPositionMenu.dispose(); + createAndFillInContextMenuActions(panelPositionMenu, { primary: [], secondary: positionActions }); + createAndFillInContextMenuActions(panelAlignMenu, { primary: [], secondary: alignActions }); actions.push(...[ new Separator(), diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index d6897bf02d7..b30d2bee088 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -88,6 +88,11 @@ margin-left: 3px; } +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-left.compact-right > .statusbar-item-label { + margin-right:0; + margin-left: 0; +} + .monaco-workbench .part.statusbar > .items-container > .statusbar-item.left.first-visible-item { padding-left: 7px; /* Add padding to the most left status bar item */ } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts index 07dd5640c0c..d458ce91e79 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts @@ -24,7 +24,7 @@ import { spinningLoading, syncing } from 'vs/platform/theme/common/iconRegistry' import { isMarkdownString, markdownStringEqual } from 'vs/base/common/htmlContent'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; export class StatusbarEntryItem extends Disposable { @@ -42,7 +42,7 @@ export class StatusbarEntryItem extends Disposable { private readonly focusListener = this._register(new MutableDisposable()); private readonly focusOutListener = this._register(new MutableDisposable()); - private hover: IUpdatableHover | undefined = undefined; + private hover: IManagedHover | undefined = undefined; readonly labelContainer: HTMLElement; readonly beakContainer: HTMLElement; @@ -122,7 +122,7 @@ export class StatusbarEntryItem extends Disposable { if (this.hover) { this.hover.update(hoverContents); } else { - this.hover = this._register(this.hoverService.setupUpdatableHover(this.hoverDelegate, this.container, hoverContents)); + this.hover = this._register(this.hoverService.setupManagedHover(this.hoverDelegate, this.container, hoverContents)); } if (entry.command !== ShowTooltipCommand /* prevents flicker on click */) { this.focusListener.value = addDisposableListener(this.labelContainer, EventType.FOCUS, e => { @@ -283,7 +283,7 @@ class StatusBarCodiconLabel extends SimpleIconLabel { private progressCodicon = renderIcon(syncing); private currentText = ''; - private currentShowProgress: boolean | 'syncing' | 'loading' = false; + private currentShowProgress: boolean | 'loading' | 'syncing' = false; constructor( private readonly container: HTMLElement @@ -291,10 +291,10 @@ class StatusBarCodiconLabel extends SimpleIconLabel { super(container); } - set showProgress(showProgress: boolean | 'syncing' | 'loading') { + set showProgress(showProgress: boolean | 'loading' | 'syncing') { if (this.currentShowProgress !== showProgress) { this.currentShowProgress = showProgress; - this.progressCodicon = renderIcon(showProgress === 'loading' ? spinningLoading : syncing); + this.progressCodicon = renderIcon(showProgress === 'syncing' ? syncing : spinningLoading); this.text = this.currentText; } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index 8fd7e353217..8c0ecf356a2 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -258,19 +258,34 @@ export class StatusbarViewModel extends Disposable { // - those with `priority: number` that can be compared // - those with `priority: string` that must be sorted // relative to another entry if possible - const mapEntryWithNumberedPriorityToIndex = new Map(); - const mapEntryWithRelativePriority = new Map(); + const mapEntryWithNumberedPriorityToIndex = new Map(); + const mapEntryWithRelativePriority = new Map>(); for (let i = 0; i < this._entries.length; i++) { const entry = this._entries[i]; if (typeof entry.priority.primary === 'number') { mapEntryWithNumberedPriorityToIndex.set(entry, i); } else { - let entries = mapEntryWithRelativePriority.get(entry.priority.primary.id); + const referenceEntryId = entry.priority.primary.id; + let entries = mapEntryWithRelativePriority.get(referenceEntryId); if (!entries) { - entries = []; - mapEntryWithRelativePriority.set(entry.priority.primary.id, entries); + + // It is possible that this entry references another entry + // that itself references an entry. In that case, we want + // to add it to the entries of the referenced entry. + + for (const relativeEntries of mapEntryWithRelativePriority.values()) { + if (relativeEntries.has(referenceEntryId)) { + entries = relativeEntries; + break; + } + } + + if (!entries) { + entries = new Map(); + mapEntryWithRelativePriority.set(referenceEntryId, entries); + } } - entries.push(entry); + entries.set(entry.id, entry); } } @@ -311,7 +326,8 @@ export class StatusbarViewModel extends Disposable { sortedEntries = []; for (const entry of sortedEntriesWithNumberedPriority) { - const relativeEntries = mapEntryWithRelativePriority.get(entry.id); + const relativeEntriesMap = mapEntryWithRelativePriority.get(entry.id); + const relativeEntries = relativeEntriesMap ? Array.from(relativeEntriesMap.values()) : undefined; // Fill relative entries to LEFT if (relativeEntries) { @@ -333,7 +349,7 @@ export class StatusbarViewModel extends Disposable { // Finally, just append all entries that reference another entry // that does not exist to the end of the list for (const [, entries] of mapEntryWithRelativePriority) { - sortedEntries.push(...entries); + sortedEntries.push(...entries.values()); } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index f938ea7b353..bac67111eae 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -433,7 +433,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { } // Figure out groups of entries with `compact` alignment - const compactEntryGroups = new Map>(); + const compactEntryGroups = new Map>(); for (const entry of mapIdToVisibleEntry.values()) { if ( isStatusbarEntryLocation(entry.priority.primary) && // entry references another entry as location @@ -448,11 +448,25 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { // Build a map of entries that are compact among each other let compactEntryGroup = compactEntryGroups.get(locationId); if (!compactEntryGroup) { - compactEntryGroup = new Set([entry, location]); - compactEntryGroups.set(locationId, compactEntryGroup); - } else { - compactEntryGroup.add(entry); + + // It is possible that this entry references another entry + // that itself references an entry. In that case, we want + // to add it to the entries of the referenced entry. + + for (const group of compactEntryGroups.values()) { + if (group.has(locationId)) { + compactEntryGroup = group; + break; + } + } + + if (!compactEntryGroup) { + compactEntryGroup = new Map(); + compactEntryGroups.set(locationId, compactEntryGroup); + } } + compactEntryGroup.set(entry.id, entry); + compactEntryGroup.set(location.id, location); // Adjust CSS classes to move compact items closer together if (entry.priority.primary.alignment === StatusbarAlignment.LEFT) { @@ -465,7 +479,6 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { } } - // Install mouse listeners to update hover feedback for // all compact entries that belong to each other const statusBarItemHoverBackground = this.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); @@ -473,7 +486,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { this.compactEntriesDisposable.value = new DisposableStore(); if (statusBarItemHoverBackground && statusBarItemCompactHoverBackground && !isHighContrast(this.theme.type)) { for (const [, compactEntryGroup] of compactEntryGroups) { - for (const compactEntry of compactEntryGroup) { + for (const compactEntry of compactEntryGroup.values()) { if (!compactEntry.hasCommand) { continue; // only show hover feedback when we have a command } diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 59b379f497d..88d5435936b 100644 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -96,7 +96,7 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { container.classList.add('command-center-center'); container.classList.toggle('multiple', (this._submenu.actions.length > 1)); - const hover = this._store.add(this._hoverService.setupUpdatableHover(this._hoverDelegate, container, this.getTooltip())); + const hover = this._store.add(this._hoverService.setupManagedHover(this._hoverDelegate, container, this.getTooltip())); // update label & tooltip when window title changes this._store.add(this._windowTitle.onDidChange(() => { @@ -157,7 +157,7 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { labelElement.innerText = label; reset(container, searchIcon, labelElement); - const hover = this._store.add(that._hoverService.setupUpdatableHover(that._hoverDelegate, container, this.getTooltip())); + const hover = this._store.add(that._hoverService.setupManagedHover(that._hoverDelegate, container, this.getTooltip())); // update label & tooltip when window title changes this._store.add(that._windowTitle.onDidChange(() => { diff --git a/src/vs/workbench/browser/parts/titlebar/media/menubarControl.css b/src/vs/workbench/browser/parts/titlebar/media/menubarControl.css index b41a6f925da..923c221df25 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/menubarControl.css +++ b/src/vs/workbench/browser/parts/titlebar/media/menubarControl.css @@ -22,6 +22,10 @@ color: var(--vscode-activityBar-foreground); } +.monaco-workbench .activitybar .menubar.compact > .menubar-menu-button:focus { + background-color: var(--vscode-menubar-selectionBackground); +} + .monaco-workbench .menubar.inactive:not(.compact) > .menubar-menu-button, .monaco-workbench .menubar.inactive:not(.compact) > .menubar-menu-button .toolbar-toggle-more { color: var(--vscode-titleBar-inactiveForeground); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 0bee377dce9..f610512e84f 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/titlebarpart'; import { localize, localize2 } from 'vs/nls'; import { MultiWindowParts, Part } from 'vs/workbench/browser/part'; import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; -import { getZoomFactor, isWCOEnabled } from 'vs/base/browser/browser'; +import { getWCOBoundingRect, getZoomFactor, isWCOEnabled } from 'vs/base/browser/browser'; import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, TitlebarStyle, hasCustomTitlebar, hasNativeTitlebar, DEFAULT_CUSTOM_TITLEBAR_HEIGHT } from 'vs/platform/window/common/window'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -19,7 +19,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER, WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { isMacintosh, isWindows, isLinux, isWeb, isNative, platformLocale } from 'vs/base/common/platform'; import { Color } from 'vs/base/common/color'; -import { EventType, EventHelper, Dimension, append, $, addDisposableListener, prepend, reset, getWindow, getWindowId, isAncestor, getActiveDocument } from 'vs/base/browser/dom'; +import { EventType, EventHelper, Dimension, append, $, addDisposableListener, prepend, reset, getWindow, getWindowId, isAncestor, getActiveDocument, isHTMLElement } from 'vs/base/browser/dom'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -200,7 +200,11 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { readonly maximumWidth: number = Number.POSITIVE_INFINITY; get minimumHeight(): number { - const value = this.isCommandCenterVisible || (isWeb && isWCOEnabled()) ? DEFAULT_CUSTOM_TITLEBAR_HEIGHT : 30; + const wcoEnabled = isWeb && isWCOEnabled(); + let value = this.isCommandCenterVisible || wcoEnabled ? DEFAULT_CUSTOM_TITLEBAR_HEIGHT : 30; + if (wcoEnabled) { + value = Math.max(value, getWCOBoundingRect()?.height ?? 0); + } return value / (this.preventZoom ? getZoomFactor(getWindow(this.element)) : 1); } @@ -474,7 +478,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { EventHelper.stop(e); let targetMenu: MenuId; - if (isMacintosh && e.target instanceof HTMLElement && isAncestor(e.target, this.title)) { + if (isMacintosh && isHTMLElement(e.target) && isAncestor(e.target, this.title)) { targetMenu = MenuId.TitleBarTitleContext; } else { targetMenu = MenuId.TitleBarContext; @@ -634,7 +638,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this.editorToolbarMenuDisposables.add(this.actionToolBar.actionRunner); } else { this.actionToolBar.actionRunner = new ActionRunner(); - this.actionToolBar.context = {}; + this.actionToolBar.context = undefined; this.editorToolbarMenuDisposables.add(this.actionToolBar.actionRunner); } diff --git a/src/vs/workbench/browser/parts/views/checkbox.ts b/src/vs/workbench/browser/parts/views/checkbox.ts index 6d6125e4f5c..f428de9bffe 100644 --- a/src/vs/workbench/browser/parts/views/checkbox.ts +++ b/src/vs/workbench/browser/parts/views/checkbox.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Codicon } from 'vs/base/common/codicons'; @@ -28,7 +28,7 @@ export class TreeItemCheckbox extends Disposable { public toggle: Toggle | undefined; private checkboxContainer: HTMLDivElement; public isDisposed = false; - private hover: IUpdatableHover | undefined; + private hover: IManagedHover | undefined; public static readonly checkboxClass = 'custom-view-tree-node-item-checkbox'; @@ -87,7 +87,7 @@ export class TreeItemCheckbox extends Disposable { private setHover(checkbox: ITreeItemCheckboxState) { if (this.toggle) { if (!this.hover) { - this.hover = this._register(this.hoverService.setupUpdatableHover(this.hoverDelegate, this.toggle.domNode, this.checkboxHoverContent(checkbox))); + this.hover = this._register(this.hoverService.setupManagedHover(this.hoverDelegate, this.toggle.domNode, this.checkboxHoverContent(checkbox))); } else { this.hover.update(checkbox.tooltip); } @@ -122,7 +122,7 @@ export class TreeItemCheckbox extends Disposable { private removeCheckbox() { const children = this.checkboxContainer.children; for (const child of children) { - this.checkboxContainer.removeChild(child); + child.remove(); } } } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index becd185fd60..5b82b9f971b 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -36,7 +36,7 @@ import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/plat import { Action2, IMenuService, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression, IContextKey, IContextKeyChangeEvent, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { FileKind } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -70,11 +70,12 @@ import { TelemetryTrustedValue } from 'vs/platform/telemetry/common/telemetryUti import { ITreeViewsDnDService } from 'vs/editor/common/services/treeViewsDndService'; import { DraggedTreeItemsIdentifier } from 'vs/editor/common/services/treeViewsDnd'; import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; -import type { IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { parseLinkedText } from 'vs/base/common/linkedText'; import { Button } from 'vs/base/browser/ui/button/button'; import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IAccessibleViewInformationService } from 'vs/workbench/services/accessibility/common/accessibleViewInformationService'; +import { Command } from 'vs/editor/common/languages'; export class TreeViewPane extends ViewPane { @@ -175,15 +176,22 @@ class Root implements ITreeItem { children: ITreeItem[] | undefined = undefined; } -function isTreeCommandEnabled(treeCommand: TreeCommand, contextKeyService: IContextKeyService): boolean { - const command = CommandsRegistry.getCommand(treeCommand.originalId ? treeCommand.originalId : treeCommand.id); +function commandPreconditions(commandId: string): ContextKeyExpression | undefined { + const command = CommandsRegistry.getCommand(commandId); if (command) { const commandAction = MenuRegistry.getCommand(command.id); - const precondition = commandAction && commandAction.precondition; - if (precondition) { - return contextKeyService.contextMatchesRules(precondition); - } + return commandAction && commandAction.precondition; } + return undefined; +} + +function isTreeCommandEnabled(treeCommand: TreeCommand | Command, contextKeyService: IContextKeyService): boolean { + const commandId: string = (treeCommand as TreeCommand).originalId ? (treeCommand as TreeCommand).originalId! : treeCommand.id; + const precondition = commandPreconditions(commandId); + if (precondition) { + return contextKeyService.contextMatchesRules(precondition); + } + return true; } @@ -709,6 +717,9 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { dnd: this.treeViewDnd, overrideStyles: getLocationBasedViewColors(this.viewLocation).listOverrideStyles }) as WorkbenchAsyncDataTree); + + this.treeDisposables.add(renderer.onDidChangeMenuContext(e => e.forEach(e => this.tree?.rerender(e)))); + this.treeDisposables.add(this.tree); treeMenus.setContextKeyService(this.tree.contextKeyService); aligner.tree = this.tree; @@ -863,6 +874,20 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { button.onDidClick(_ => { this.openerService.open(node.href, { allowCommands: true }); }, null, disposables); + + const href = URI.parse(node.href); + if (href.scheme === Schemas.command) { + const preConditions = commandPreconditions(href.path); + if (preConditions) { + button.enabled = this.contextKeyService.contextMatchesRules(preConditions); + disposables.add(this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(new Set(preConditions.keys()))) { + button.enabled = this.contextKeyService.contextMatchesRules(preConditions); + } + })); + } + } + disposables.add(button); hasFoundButton = true; result.push(buttonContainer); @@ -877,7 +902,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { const container = document.createElement('div'); container.classList.add('rendered-message'); for (const child of result) { - if (child instanceof HTMLElement) { + if (DOM.isHTMLElement(child)) { container.appendChild(child); } else { container.appendChild(child.element); @@ -1135,7 +1160,6 @@ class TreeDataSource implements IAsyncDataSource { } interface ITreeExplorerTemplateData { - readonly elementDisposable: DisposableStore; readonly container: HTMLElement; readonly resourceLabel: IResourceLabel; readonly icon: HTMLElement; @@ -1151,6 +1175,9 @@ class TreeRenderer extends Disposable implements ITreeRenderer = this._register(new Emitter()); readonly onDidChangeCheckboxState: Event = this._onDidChangeCheckboxState.event; + private _onDidChangeMenuContext: Emitter = this._register(new Emitter()); + readonly onDidChangeMenuContext: Event = this._onDidChangeMenuContext.event; + private _actionRunner: MultipleSelectionActionRunner | undefined; private _hoverDelegate: IHoverDelegate; private _hasCheckbox: boolean = false; @@ -1178,6 +1205,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer { this.updateCheckboxes(items); })); + this._register(this.contextKeyService.onDidChangeContext(e => this.onDidChangeContext(e))); } get templateId(): string { @@ -1199,10 +1227,10 @@ class TreeRenderer extends Disposable implements ITreeRenderer, index: number, templateData: ITreeExplorerTemplateData): void { - templateData.elementDisposable.clear(); - const itemRenders = this._renderedElements.get(resource.element.handle) ?? []; const renderedIndex = itemRenders.findIndex(renderedItem => templateData === renderedItem.rendered); @@ -1491,7 +1531,6 @@ class TreeRenderer extends Disposable implements ITreeRenderer { + return new Map([ + ['view', this.id], + ['viewItem', element.contextValue] + ]); + } + + public getEntireMenuContexts(): ReadonlySet { + return this.menuService.getMenuContexts(this.getMenuId()); + } + + public getMenuId(): MenuId { + return MenuId.ViewItemContext; + } + + private getActions(menuId: MenuId, elements: ITreeItem[]): { primary: IAction[]; secondary: IAction[] } { if (!this.contextKeyService) { return { primary: [], secondary: [] }; } @@ -1646,16 +1706,14 @@ class TreeMenus implements IDisposable { let secondaryGroups: Map[] = []; for (let i = 0; i < elements.length; i++) { const element = elements[i]; - const contextKeyService = this.contextKeyService.createOverlay([ - ['view', this.id], - ['viewItem', element.contextValue] - ]); + const contextKeyService = this.contextKeyService.createOverlay(this.getElementOverlayContexts(element)); + + const menuData = this.menuService.getMenuActions(menuId, contextKeyService, { shouldForwardArgs: true }); - const menu = this.menuService.createMenu(menuId, contextKeyService); const primary: IAction[] = []; const secondary: IAction[] = []; - const result = { primary, secondary, menu }; - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, 'inline'); + const result = { primary, secondary }; + createAndFillInContextMenuActions(menuData, result, 'inline'); if (i === 0) { primaryGroups = this.createGroups(result.primary); secondaryGroups = this.createGroups(result.secondary); @@ -1663,12 +1721,6 @@ class TreeMenus implements IDisposable { this.filterNonUniversalActions(primaryGroups, result.primary); this.filterNonUniversalActions(secondaryGroups, result.secondary); } - if (listen && elements.length === 1) { - listen.add(menu.onDidChange(() => this._onDidChange.fire(element))); - listen.add(menu); - } else { - menu.dispose(); - } } return { primary: this.buildMenu(primaryGroups), secondary: this.buildMenu(secondaryGroups) }; diff --git a/src/vs/workbench/browser/parts/views/viewFilter.ts b/src/vs/workbench/browser/parts/views/viewFilter.ts index 3331c892d0c..724a67b931a 100644 --- a/src/vs/workbench/browser/parts/views/viewFilter.ts +++ b/src/vs/workbench/browser/parts/views/viewFilter.ts @@ -230,6 +230,8 @@ export class FilterWidget extends Widget { if (event.equals(KeyCode.Space) || event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow) + || event.equals(KeyCode.Home) + || event.equals(KeyCode.End) ) { event.stopPropagation(); } diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index c62f1b0ab47..28a82a629df 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -48,7 +48,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { defaultButtonStyles, defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { PANEL_BACKGROUND, PANEL_STICKY_SCROLL_BACKGROUND, PANEL_STICKY_SCROLL_BORDER, PANEL_STICKY_SCROLL_SHADOW, SIDE_BAR_BACKGROUND, SIDE_BAR_STICKY_SCROLL_BACKGROUND, SIDE_BAR_STICKY_SCROLL_BORDER, SIDE_BAR_STICKY_SCROLL_SHADOW } from 'vs/workbench/common/theme'; @@ -354,11 +354,11 @@ export abstract class ViewPane extends Pane implements IView { private readonly showActions: ViewPaneShowActions; private headerContainer?: HTMLElement; private titleContainer?: HTMLElement; - private titleContainerHover?: IUpdatableHover; + private titleContainerHover?: IManagedHover; private titleDescriptionContainer?: HTMLElement; - private titleDescriptionContainerHover?: IUpdatableHover; + private titleDescriptionContainerHover?: IManagedHover; private iconContainer?: HTMLElement; - private iconContainerHover?: IUpdatableHover; + private iconContainerHover?: IManagedHover; protected twistiesContainer?: HTMLElement; private viewWelcomeController!: ViewWelcomeController; @@ -391,7 +391,8 @@ export abstract class ViewPane extends Pane implements IView { const viewLocationKey = this.scopedContextKeyService.createKey('viewLocation', ViewContainerLocationToString(viewDescriptorService.getViewLocationById(this.id)!)); this._register(Event.filter(viewDescriptorService.onDidChangeLocation, e => e.views.some(view => view.id === this.id))(() => viewLocationKey.set(ViewContainerLocationToString(viewDescriptorService.getViewLocationById(this.id)!)))); - this.menuActions = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])).createInstance(CompositeMenuActions, options.titleMenuId ?? MenuId.ViewTitle, MenuId.ViewTitleContext, { shouldForwardArgs: !options.donotForwardArgs, renderShortTitle: true })); + const childInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); + this.menuActions = this._register(childInstantiationService.createInstance(CompositeMenuActions, options.titleMenuId ?? MenuId.ViewTitle, MenuId.ViewTitleContext, { shouldForwardArgs: !options.donotForwardArgs, renderShortTitle: true })); this._register(this.menuActions.onDidChange(() => this.updateActions())); } @@ -540,13 +541,13 @@ export abstract class ViewPane extends Pane implements IView { const calculatedTitle = this.calculateTitle(title); this.titleContainer = append(container, $('h3.title', {}, calculatedTitle)); - this.titleContainerHover = this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.titleContainer, calculatedTitle)); + this.titleContainerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.titleContainer, calculatedTitle)); if (this._titleDescription) { this.setTitleDescription(this._titleDescription); } - this.iconContainerHover = this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.iconContainer, calculatedTitle)); + this.iconContainerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.iconContainer, calculatedTitle)); this.iconContainer.setAttribute('aria-label', this._getAriaLabel(calculatedTitle)); } @@ -583,7 +584,7 @@ export abstract class ViewPane extends Pane implements IView { } else if (description && this.titleContainer) { this.titleDescriptionContainer = after(this.titleContainer, $('span.description', {}, description)); - this.titleDescriptionContainerHover = this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.titleDescriptionContainer, description)); + this.titleDescriptionContainerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.titleDescriptionContainer, description)); } } @@ -747,7 +748,8 @@ export abstract class FilterViewPane extends ViewPane { accessibleViewService?: IAccessibleViewInformationService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService, accessibleViewService); - this.filterWidget = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])).createInstance(FilterWidget, options.filterOptions)); + const childInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); + this.filterWidget = this._register(childInstantiationService.createInstance(FilterWidget, options.filterOptions)); } override getFilterWidget(): FilterWidget { diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 38459b48cdc..d782b08c3c2 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -112,7 +112,7 @@ class ViewPaneDropOverlay extends Themable { this.paneElement.appendChild(this.container); this.paneElement.classList.add('dragged-over'); this._register(toDisposable(() => { - this.paneElement.removeChild(this.container); + this.container.remove(); this.paneElement.classList.remove('dragged-over'); })); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index ce003932bf3..fb05198e50b 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -6,8 +6,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { localize } from 'vs/nls'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { isMacintosh, isWindows, isLinux, isWeb } from 'vs/base/common/platform'; -import { ConfigurationMigrationWorkbenchContribution, DynamicWorkbenchSecurityConfiguration, IConfigurationMigrationRegistry, workbenchConfigurationNodeBase, Extensions, ConfigurationKeyValuePairs, problemsConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { isMacintosh, isWindows, isLinux, isWeb, isNative } from 'vs/base/common/platform'; +import { ConfigurationMigrationWorkbenchContribution, DynamicWorkbenchSecurityConfiguration, IConfigurationMigrationRegistry, workbenchConfigurationNodeBase, Extensions, ConfigurationKeyValuePairs, problemsConfigurationNodeBase, windowConfigurationNodeBase, DynamicWindowConfiguration } from 'vs/workbench/common/configuration'; import { isStandalone } from 'vs/base/browser/browser'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { ActivityBarPosition, EditorActionsLocation, EditorTabsMode, LayoutSettings } from 'vs/workbench/services/layout/browser/layoutService'; @@ -29,6 +29,12 @@ const registry = Registry.as(ConfigurationExtensions.Con registry.registerConfiguration({ ...workbenchConfigurationNodeBase, 'properties': { + 'workbench.externalBrowser': { + type: 'string', + markdownDescription: localize('browser', "Configure the browser to use for opening http or https links externally. This can either be the name of the browser (`edge`, `chrome`, `firefox`) or an absolute path to the browser's executable. Will use the system default if not set."), + included: isNative, + restricted: true + }, 'workbench.editor.titleScrollbarSizing': { type: 'string', enum: ['default', 'large'], @@ -102,9 +108,10 @@ const registry = Registry.as(ConfigurationExtensions.Con let customEditorLabelDescription = localize('workbench.editor.label.patterns', "Controls the rendering of the editor label. Each __Item__ is a pattern that matches a file path. Both relative and absolute file paths are supported. The relative path must include the WORKSPACE_FOLDER (e.g `WORKSPACE_FOLDER/src/**.tsx` or `*/src/**.tsx`). Absolute patterns must start with a `/`. In case multiple patterns match, the longest matching path will be picked. Each __Value__ is the template for the rendered editor when the __Item__ matches. Variables are substituted based on the context:"); customEditorLabelDescription += '\n- ' + [ localize('workbench.editor.label.dirname', "`${dirname}`: name of the folder in which the file is located (e.g. `WORKSPACE_FOLDER/folder/file.txt -> folder`)."), - localize('workbench.editor.label.nthdirname', "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=2: WORKSPACE_FOLDER/static/folder/file.txt -> WORKSPACE_FOLDER`). Folders can be picked from the start of the path by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.txt -> WORKSPACE_FOLDER`). If the __Item__ is an absolute pattern path, the first folder (`N=-1`) refers to the first folder in the absoulte path, otherwise it corresponds to the workspace folder."), + localize('workbench.editor.label.nthdirname', "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=2: WORKSPACE_FOLDER/static/folder/file.txt -> WORKSPACE_FOLDER`). Folders can be picked from the start of the path by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.txt -> WORKSPACE_FOLDER`). If the __Item__ is an absolute pattern path, the first folder (`N=-1`) refers to the first folder in the absolute path, otherwise it corresponds to the workspace folder."), localize('workbench.editor.label.filename', "`${filename}`: name of the file without the file extension (e.g. `WORKSPACE_FOLDER/folder/file.txt -> file`)."), localize('workbench.editor.label.extname', "`${extname}`: the file extension (e.g. `WORKSPACE_FOLDER/folder/file.txt -> txt`)."), + localize('workbench.editor.label.nthextname', "`${extname(N)}`: the nth extension of the file separated by '.' (e.g. `N=2: WORKSPACE_FOLDER/folder/file.ext1.ext2.ext3 -> ext1`). Extension can be picked from the start of the extension by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.ext1.ext2.ext3 -> ext2`)."), ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations customEditorLabelDescription += '\n\n' + localize('customEditorLabelDescriptionExample', "Example: `\"**/static/**/*.html\": \"${filename} - ${dirname} (${extname})\"` will render a file `WORKSPACE_FOLDER/static/folder/file.html` as `file - folder (html)`."); @@ -112,8 +119,8 @@ const registry = Registry.as(ConfigurationExtensions.Con })(), additionalProperties: { - type: 'string', - markdownDescription: localize('workbench.editor.label.template', "The template which should be rendered when the pattern mtches. May include the variables ${dirname}, ${filename} and ${extname}."), + type: ['string', 'null'], + markdownDescription: localize('workbench.editor.label.template', "The template which should be rendered when the pattern matches. May include the variables ${dirname}, ${filename} and ${extname}."), minLength: 1, pattern: '.*[a-zA-Z0-9].*' }, @@ -653,10 +660,7 @@ const registry = Registry.as(ConfigurationExtensions.Con ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations registry.registerConfiguration({ - 'id': 'window', - 'order': 8, - 'title': localize('windowConfigurationTitle', "Window"), - 'type': 'object', + ...windowConfigurationNodeBase, 'properties': { 'window.title': { 'type': 'string', @@ -762,6 +766,9 @@ const registry = Registry.as(ConfigurationExtensions.Con } }); + // Dynamic Window Configuration + registerWorkbenchContribution2(DynamicWindowConfiguration.ID, DynamicWindowConfiguration, WorkbenchPhase.Eventually); + // Problems registry.registerConfiguration({ ...problemsConfigurationNodeBase, diff --git a/src/vs/workbench/common/configuration.ts b/src/vs/workbench/common/configuration.ts index 4e15bd458be..f4aff57ea85 100644 --- a/src/vs/workbench/common/configuration.ts +++ b/src/vs/workbench/common/configuration.ts @@ -16,6 +16,7 @@ import { OperatingSystem, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { equals } from 'vs/base/common/objects'; import { DeferredPromise } from 'vs/base/common/async'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export const applicationConfigurationNodeBase = Object.freeze({ 'id': 'application', @@ -46,6 +47,13 @@ export const problemsConfigurationNodeBase = Object.freeze({ 'order': 101 }); +export const windowConfigurationNodeBase = Object.freeze({ + 'id': 'window', + 'order': 8, + 'title': localize('windowConfigurationTitle', "Window"), + 'type': 'object', +}); + export const Extensions = { ConfigurationMigration: 'base.contributions.configuration.migration' }; @@ -225,3 +233,72 @@ export class DynamicWorkbenchSecurityConfiguration extends Disposable implements }); } } + +export const CONFIG_NEW_WINDOW_PROFILE = 'window.newWindowProfile'; + +export class DynamicWindowConfiguration extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.dynamicWindowConfiguration'; + + private configurationNode: IConfigurationNode | undefined; + private newWindowProfile: IUserDataProfile | undefined; + + constructor( + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(); + this.registerNewWindowProfileConfiguration(); + this._register(this.userDataProfilesService.onDidChangeProfiles((e) => this.registerNewWindowProfileConfiguration())); + + this.setNewWindowProfile(); + this.checkAndResetNewWindowProfileConfig(); + + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.source !== ConfigurationTarget.DEFAULT && e.affectsConfiguration(CONFIG_NEW_WINDOW_PROFILE)) { + this.setNewWindowProfile(); + } + })); + this._register(this.userDataProfilesService.onDidChangeProfiles(() => this.checkAndResetNewWindowProfileConfig())); + } + + private registerNewWindowProfileConfiguration(): void { + const registry = Registry.as(ConfigurationExtensions.Configuration); + const configurationNode: IConfigurationNode = { + ...windowConfigurationNodeBase, + 'properties': { + [CONFIG_NEW_WINDOW_PROFILE]: { + 'type': ['string', 'null'], + 'default': null, + 'enum': [...this.userDataProfilesService.profiles.map(profile => profile.name), null], + 'enumItemLabels': [...this.userDataProfilesService.profiles.map(p => ''), localize('active window', "Active Window")], + 'description': localize('newWindowProfile', "Specifies the profile to use when opening a new window. If a profile name is provided, the new window will use that profile. If no profile name is provided, the new window will use the profile of the active window or the default profile if no active window exists."), + 'scope': ConfigurationScope.APPLICATION, + } + } + }; + if (this.configurationNode) { + registry.updateConfigurations({ add: [configurationNode], remove: [this.configurationNode] }); + } else { + registry.registerConfiguration(configurationNode); + } + this.configurationNode = configurationNode; + } + + private setNewWindowProfile(): void { + const newWindowProfileName = this.configurationService.getValue(CONFIG_NEW_WINDOW_PROFILE); + this.newWindowProfile = newWindowProfileName ? this.userDataProfilesService.profiles.find(profile => profile.name === newWindowProfileName) : undefined; + } + + private checkAndResetNewWindowProfileConfig(): void { + const newWindowProfileName = this.configurationService.getValue(CONFIG_NEW_WINDOW_PROFILE); + if (!newWindowProfileName) { + return; + } + const profile = this.newWindowProfile ? this.userDataProfilesService.profiles.find(profile => profile.id === this.newWindowProfile!.id) : undefined; + if (newWindowProfileName === profile?.name) { + return; + } + this.configurationService.updateValue(CONFIG_NEW_WINDOW_PROFILE, profile?.name); + } +} diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 97937218bbb..d02f9e0e89d 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -74,6 +74,7 @@ export const MultipleEditorGroupsContext = new RawContextKey('multipleE export const SingleEditorGroupsContext = MultipleEditorGroupsContext.toNegated(); export const MultipleEditorsSelectedInGroupContext = new RawContextKey('multipleEditorsSelectedInGroup', false, localize('multipleEditorsSelectedInGroup', "Whether multiple editors have been selected in an editor group")); export const TwoEditorsSelectedInGroupContext = new RawContextKey('twoEditorsSelectedInGroup', false, localize('twoEditorsSelectedInGroup', "Whether exactly two editors have been selected in an editor group")); +export const SelectedEditorsInGroupFileOrUntitledResourceContextKey = new RawContextKey('SelectedEditorsInGroupFileOrUntitledResourceContextKey', true, localize('SelectedEditorsInGroupFileOrUntitledResourceContextKey', "Whether all selected editors in a group have a file or untitled resource associated")); // Editor Part Context Keys export const EditorPartMultipleEditorGroupsContext = new RawContextKey('editorPartMultipleEditorGroups', false, localize('editorPartMultipleEditorGroups', "Whether there are multiple editor groups opened in an editor part")); diff --git a/src/vs/workbench/common/contributions.ts b/src/vs/workbench/common/contributions.ts index aaf1452c25a..f7e23fae0a2 100644 --- a/src/vs/workbench/common/contributions.ts +++ b/src/vs/workbench/common/contributions.ts @@ -11,7 +11,7 @@ import { mark } from 'vs/base/common/performance'; import { ILogService } from 'vs/platform/log/common/log'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { getOrSet } from 'vs/base/common/map'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, isDisposable } from 'vs/base/common/lifecycle'; import { IEditorPaneService } from 'vs/workbench/services/editor/common/editorPaneService'; /** @@ -156,6 +156,7 @@ export class WorkbenchContributionsRegistry extends Disposable implements IWorkb private readonly contributionsById = new Map(); private readonly instancesById = new Map(); + private readonly instanceDisposables = this._register(new DisposableStore()); private readonly timingsByPhase = new Map>(); get timings() { return this.timingsByPhase; } @@ -249,6 +250,11 @@ export class WorkbenchContributionsRegistry extends Disposable implements IWorkb const environmentService = this.environmentService = accessor.get(IEnvironmentService); const editorPaneService = this.editorPaneService = accessor.get(IEditorPaneService); + // Dispose contributions on shutdown + this._register(lifecycleService.onDidShutdown(() => { + this.instanceDisposables.clear(); + })); + // Instantiate contributions by phase when they are ready for (const phase of [LifecyclePhase.Starting, LifecyclePhase.Ready, LifecyclePhase.Restored, LifecyclePhase.Eventually]) { this.instantiateByPhase(instantiationService, lifecycleService, logService, environmentService, phase); @@ -377,6 +383,9 @@ export class WorkbenchContributionsRegistry extends Disposable implements IWorkb this.instancesById.set(contribution.id, instance); this.contributionsById.delete(contribution.id); } + if (isDisposable(instance)) { + this.instanceDisposables.add(instance); + } } catch (error) { logService.error(`Unable to create workbench contribution '${contribution.id ?? contribution.ctor.name}'.`, error); } finally { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 02aff158f4f..d3d6adb4ff5 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -547,7 +547,7 @@ export interface IResourceMultiDiffEditorInput extends IBaseUntypedEditorInput { * The list of resources to compare. * If not set, the resources are dynamically derived from the {@link multiDiffSource}. */ - readonly resources?: IResourceDiffEditorInput[]; + readonly resources?: IMultiDiffEditorResource[]; /** * Whether the editor should be serialized and stored for subsequent sessions. @@ -555,6 +555,9 @@ export interface IResourceMultiDiffEditorInput extends IBaseUntypedEditorInput { readonly isTransient?: boolean; } +export interface IMultiDiffEditorResource extends IResourceDiffEditorInput { + readonly goToFileResource?: URI; +} export type IResourceMergeEditorInputSide = (IResourceEditorInput | ITextResourceEditorInput) & { detail?: string }; /** diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 3412f81c3bf..1e6aba052fd 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -28,19 +28,9 @@ export function WORKBENCH_BACKGROUND(theme: IColorTheme): Color { //#region Tab Background -export const TAB_ACTIVE_BACKGROUND = registerColor('tab.activeBackground', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('tabActiveBackground', "Active tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_ACTIVE_BACKGROUND = registerColor('tab.activeBackground', editorBackground, localize('tabActiveBackground', "Active tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_UNFOCUSED_ACTIVE_BACKGROUND = registerColor('tab.unfocusedActiveBackground', { - dark: TAB_ACTIVE_BACKGROUND, - light: TAB_ACTIVE_BACKGROUND, - hcDark: TAB_ACTIVE_BACKGROUND, - hcLight: TAB_ACTIVE_BACKGROUND, -}, localize('tabUnfocusedActiveBackground', "Active tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_UNFOCUSED_ACTIVE_BACKGROUND = registerColor('tab.unfocusedActiveBackground', TAB_ACTIVE_BACKGROUND, localize('tabUnfocusedActiveBackground', "Active tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_INACTIVE_BACKGROUND = registerColor('tab.inactiveBackground', { dark: '#2D2D2D', @@ -49,12 +39,7 @@ export const TAB_INACTIVE_BACKGROUND = registerColor('tab.inactiveBackground', { hcLight: null, }, localize('tabInactiveBackground', "Inactive tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_UNFOCUSED_INACTIVE_BACKGROUND = registerColor('tab.unfocusedInactiveBackground', { - dark: TAB_INACTIVE_BACKGROUND, - light: TAB_INACTIVE_BACKGROUND, - hcDark: TAB_INACTIVE_BACKGROUND, - hcLight: TAB_INACTIVE_BACKGROUND -}, localize('tabUnfocusedInactiveBackground', "Inactive tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_UNFOCUSED_INACTIVE_BACKGROUND = registerColor('tab.unfocusedInactiveBackground', TAB_INACTIVE_BACKGROUND, localize('tabUnfocusedInactiveBackground', "Inactive tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); //#endregion @@ -92,12 +77,7 @@ export const TAB_UNFOCUSED_INACTIVE_FOREGROUND = registerColor('tab.unfocusedIna //#region Tab Hover Foreground/Background -export const TAB_HOVER_BACKGROUND = registerColor('tab.hoverBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabHoverBackground', "Tab background color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_HOVER_BACKGROUND = registerColor('tab.hoverBackground', null, localize('tabHoverBackground', "Tab background color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_BACKGROUND = registerColor('tab.unfocusedHoverBackground', { dark: transparent(TAB_HOVER_BACKGROUND, 0.5), @@ -106,12 +86,7 @@ export const TAB_UNFOCUSED_HOVER_BACKGROUND = registerColor('tab.unfocusedHoverB hcLight: null }, localize('tabUnfocusedHoverBackground', "Tab background color in an unfocused group when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_HOVER_FOREGROUND = registerColor('tab.hoverForeground', { - dark: null, - light: null, - hcDark: null, - hcLight: null, -}, localize('tabHoverForeground', "Tab foreground color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_HOVER_FOREGROUND = registerColor('tab.hoverForeground', null, localize('tabHoverForeground', "Tab foreground color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_FOREGROUND = registerColor('tab.unfocusedHoverForeground', { dark: transparent(TAB_HOVER_FOREGROUND, 0.5), @@ -138,12 +113,7 @@ export const TAB_LAST_PINNED_BORDER = registerColor('tab.lastPinnedBorder', { hcLight: contrastBorder }, localize('lastPinnedTabBorder', "Border to separate pinned tabs from other tabs. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_ACTIVE_BORDER = registerColor('tab.activeBorder', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabActiveBorder', "Border on the bottom of an active tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_ACTIVE_BORDER = registerColor('tab.activeBorder', null, localize('tabActiveBorder', "Border on the bottom of an active tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_ACTIVE_BORDER = registerColor('tab.unfocusedActiveBorder', { dark: transparent(TAB_ACTIVE_BORDER, 0.5), @@ -166,34 +136,14 @@ export const TAB_UNFOCUSED_ACTIVE_BORDER_TOP = registerColor('tab.unfocusedActiv hcLight: '#B5200D' }, localize('tabActiveUnfocusedBorderTop', "Border to the top of an active tab in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_SELECTED_BORDER_TOP = registerColor('tab.selectedBorderTop', { - dark: TAB_ACTIVE_BORDER_TOP, - light: TAB_ACTIVE_BORDER_TOP, - hcDark: TAB_ACTIVE_BORDER_TOP, - hcLight: TAB_ACTIVE_BORDER_TOP -}, localize('tabSelectedBorderTop', "Border to the top of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_SELECTED_BORDER_TOP = registerColor('tab.selectedBorderTop', TAB_ACTIVE_BORDER_TOP, localize('tabSelectedBorderTop', "Border to the top of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_SELECTED_BACKGROUND = registerColor('tab.selectedBackground', { - dark: TAB_ACTIVE_BACKGROUND, - light: TAB_ACTIVE_BACKGROUND, - hcDark: TAB_ACTIVE_BACKGROUND, - hcLight: TAB_ACTIVE_BACKGROUND -}, localize('tabSelectedBackground', "Background of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_SELECTED_BACKGROUND = registerColor('tab.selectedBackground', TAB_ACTIVE_BACKGROUND, localize('tabSelectedBackground', "Background of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_SELECTED_FOREGROUND = registerColor('tab.selectedForeground', { - dark: TAB_ACTIVE_FOREGROUND, - light: TAB_ACTIVE_FOREGROUND, - hcDark: TAB_ACTIVE_FOREGROUND, - hcLight: TAB_ACTIVE_FOREGROUND -}, localize('tabSelectedForeground', "Foreground of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_SELECTED_FOREGROUND = registerColor('tab.selectedForeground', TAB_ACTIVE_FOREGROUND, localize('tabSelectedForeground', "Foreground of a selected tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); -export const TAB_HOVER_BORDER = registerColor('tab.hoverBorder', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabHoverBorder', "Border to highlight tabs when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +export const TAB_HOVER_BORDER = registerColor('tab.hoverBorder', null, localize('tabHoverBorder', "Border to highlight tabs when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_BORDER = registerColor('tab.unfocusedHoverBorder', { dark: transparent(TAB_HOVER_BORDER, 0.5), @@ -249,19 +199,9 @@ export const TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER = registerColor('tab.unfocus // < --- Editors --- > -export const EDITOR_PANE_BACKGROUND = registerColor('editorPane.background', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('editorPaneBackground', "Background color of the editor pane visible on the left and right side of the centered editor layout.")); +export const EDITOR_PANE_BACKGROUND = registerColor('editorPane.background', editorBackground, localize('editorPaneBackground', "Background color of the editor pane visible on the left and right side of the centered editor layout.")); -export const EDITOR_GROUP_EMPTY_BACKGROUND = registerColor('editorGroup.emptyBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('editorGroupEmptyBackground', "Background color of an empty editor group. Editor groups are the containers of editors.")); +export const EDITOR_GROUP_EMPTY_BACKGROUND = registerColor('editorGroup.emptyBackground', null, localize('editorGroupEmptyBackground', "Background color of an empty editor group. Editor groups are the containers of editors.")); export const EDITOR_GROUP_FOCUSED_EMPTY_BORDER = registerColor('editorGroup.focusedEmptyBorder', { dark: null, @@ -277,19 +217,9 @@ export const EDITOR_GROUP_HEADER_TABS_BACKGROUND = registerColor('editorGroupHea hcLight: null }, localize('tabsContainerBackground', "Background color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); -export const EDITOR_GROUP_HEADER_TABS_BORDER = registerColor('editorGroupHeader.tabsBorder', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('tabsContainerBorder', "Border color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); +export const EDITOR_GROUP_HEADER_TABS_BORDER = registerColor('editorGroupHeader.tabsBorder', null, localize('tabsContainerBorder', "Border color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); -export const EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND = registerColor('editorGroupHeader.noTabsBackground', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('editorGroupHeaderBackground', "Background color of the editor group title header when (`\"workbench.editor.showTabs\": \"single\"`). Editor groups are the containers of editors.")); +export const EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND = registerColor('editorGroupHeader.noTabsBackground', editorBackground, localize('editorGroupHeaderBackground', "Background color of the editor group title header when (`\"workbench.editor.showTabs\": \"single\"`). Editor groups are the containers of editors.")); export const EDITOR_GROUP_HEADER_BORDER = registerColor('editorGroupHeader.border', { dark: null, @@ -312,19 +242,9 @@ export const EDITOR_DRAG_AND_DROP_BACKGROUND = registerColor('editorGroup.dropBa hcLight: Color.fromHex('#0F4A85').transparent(0.50) }, localize('editorDragAndDropBackground', "Background color when dragging editors around. The color should have transparency so that the editor contents can still shine through.")); -export const EDITOR_DROP_INTO_PROMPT_FOREGROUND = registerColor('editorGroup.dropIntoPromptForeground', { - dark: editorWidgetForeground, - light: editorWidgetForeground, - hcDark: editorWidgetForeground, - hcLight: editorWidgetForeground -}, localize('editorDropIntoPromptForeground', "Foreground color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); +export const EDITOR_DROP_INTO_PROMPT_FOREGROUND = registerColor('editorGroup.dropIntoPromptForeground', editorWidgetForeground, localize('editorDropIntoPromptForeground', "Foreground color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); -export const EDITOR_DROP_INTO_PROMPT_BACKGROUND = registerColor('editorGroup.dropIntoPromptBackground', { - dark: editorWidgetBackground, - light: editorWidgetBackground, - hcDark: editorWidgetBackground, - hcLight: editorWidgetBackground -}, localize('editorDropIntoPromptBackground', "Background color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); +export const EDITOR_DROP_INTO_PROMPT_BACKGROUND = registerColor('editorGroup.dropIntoPromptBackground', editorWidgetBackground, localize('editorDropIntoPromptBackground', "Background color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); export const EDITOR_DROP_INTO_PROMPT_BORDER = registerColor('editorGroup.dropIntoPromptBorder', { dark: null, @@ -333,28 +253,13 @@ export const EDITOR_DROP_INTO_PROMPT_BORDER = registerColor('editorGroup.dropInt hcLight: contrastBorder }, localize('editorDropIntoPromptBorder', "Border color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); -export const SIDE_BY_SIDE_EDITOR_HORIZONTAL_BORDER = registerColor('sideBySideEditor.horizontalBorder', { - dark: EDITOR_GROUP_BORDER, - light: EDITOR_GROUP_BORDER, - hcDark: EDITOR_GROUP_BORDER, - hcLight: EDITOR_GROUP_BORDER -}, localize('sideBySideEditor.horizontalBorder', "Color to separate two editors from each other when shown side by side in an editor group from top to bottom.")); +export const SIDE_BY_SIDE_EDITOR_HORIZONTAL_BORDER = registerColor('sideBySideEditor.horizontalBorder', EDITOR_GROUP_BORDER, localize('sideBySideEditor.horizontalBorder', "Color to separate two editors from each other when shown side by side in an editor group from top to bottom.")); -export const SIDE_BY_SIDE_EDITOR_VERTICAL_BORDER = registerColor('sideBySideEditor.verticalBorder', { - dark: EDITOR_GROUP_BORDER, - light: EDITOR_GROUP_BORDER, - hcDark: EDITOR_GROUP_BORDER, - hcLight: EDITOR_GROUP_BORDER -}, localize('sideBySideEditor.verticalBorder', "Color to separate two editors from each other when shown side by side in an editor group from left to right.")); +export const SIDE_BY_SIDE_EDITOR_VERTICAL_BORDER = registerColor('sideBySideEditor.verticalBorder', EDITOR_GROUP_BORDER, localize('sideBySideEditor.verticalBorder', "Color to separate two editors from each other when shown side by side in an editor group from left to right.")); // < --- Panels --- > -export const PANEL_BACKGROUND = registerColor('panel.background', { - dark: editorBackground, - light: editorBackground, - hcDark: editorBackground, - hcLight: editorBackground -}, localize('panelBackground', "Panel background color. Panels are shown below the editor area and contain views like output and integrated terminal.")); +export const PANEL_BACKGROUND = registerColor('panel.background', editorBackground, localize('panelBackground', "Panel background color. Panels are shown below the editor area and contain views like output and integrated terminal.")); export const PANEL_BORDER = registerColor('panel.border', { dark: Color.fromHex('#808080').transparent(0.35), @@ -391,19 +296,9 @@ export const PANEL_INPUT_BORDER = registerColor('panelInput.border', { hcLight: inputBorder }, localize('panelInputBorder', "Input box border for inputs in the panel.")); -export const PANEL_DRAG_AND_DROP_BORDER = registerColor('panel.dropBorder', { - dark: PANEL_ACTIVE_TITLE_FOREGROUND, - light: PANEL_ACTIVE_TITLE_FOREGROUND, - hcDark: PANEL_ACTIVE_TITLE_FOREGROUND, - hcLight: PANEL_ACTIVE_TITLE_FOREGROUND -}, localize('panelDragAndDropBorder', "Drag and drop feedback color for the panel titles. Panels are shown below the editor area and contain views like output and integrated terminal.")); +export const PANEL_DRAG_AND_DROP_BORDER = registerColor('panel.dropBorder', PANEL_ACTIVE_TITLE_FOREGROUND, localize('panelDragAndDropBorder', "Drag and drop feedback color for the panel titles. Panels are shown below the editor area and contain views like output and integrated terminal.")); -export const PANEL_SECTION_DRAG_AND_DROP_BACKGROUND = registerColor('panelSection.dropBackground', { - dark: EDITOR_DRAG_AND_DROP_BACKGROUND, - light: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcDark: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcLight: EDITOR_DRAG_AND_DROP_BACKGROUND -}, localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_DRAG_AND_DROP_BACKGROUND = registerColor('panelSection.dropBackground', EDITOR_DRAG_AND_DROP_BACKGROUND, localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_BACKGROUND = registerColor('panelSectionHeader.background', { dark: Color.fromHex('#808080').transparent(0.2), @@ -412,64 +307,24 @@ export const PANEL_SECTION_HEADER_BACKGROUND = registerColor('panelSectionHeader hcLight: null, }, localize('panelSectionHeaderBackground', "Panel section header background color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); -export const PANEL_SECTION_HEADER_FOREGROUND = registerColor('panelSectionHeader.foreground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_HEADER_FOREGROUND = registerColor('panelSectionHeader.foreground', null, localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); -export const PANEL_SECTION_HEADER_BORDER = registerColor('panelSectionHeader.border', { - dark: contrastBorder, - light: contrastBorder, - hcDark: contrastBorder, - hcLight: contrastBorder -}, localize('panelSectionHeaderBorder', "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_HEADER_BORDER = registerColor('panelSectionHeader.border', contrastBorder, localize('panelSectionHeaderBorder', "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); -export const PANEL_SECTION_BORDER = registerColor('panelSection.border', { - dark: PANEL_BORDER, - light: PANEL_BORDER, - hcDark: PANEL_BORDER, - hcLight: PANEL_BORDER -}, localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); +export const PANEL_SECTION_BORDER = registerColor('panelSection.border', PANEL_BORDER, localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); -export const PANEL_STICKY_SCROLL_BACKGROUND = registerColor('panelStickyScroll.background', { - dark: PANEL_BACKGROUND, - light: PANEL_BACKGROUND, - hcDark: PANEL_BACKGROUND, - hcLight: PANEL_BACKGROUND -}, localize('panelStickyScrollBackground', "Background color of sticky scroll in the panel.")); +export const PANEL_STICKY_SCROLL_BACKGROUND = registerColor('panelStickyScroll.background', PANEL_BACKGROUND, localize('panelStickyScrollBackground', "Background color of sticky scroll in the panel.")); -export const PANEL_STICKY_SCROLL_BORDER = registerColor('panelStickyScroll.border', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('panelStickyScrollBorder', "Border color of sticky scroll in the panel.")); +export const PANEL_STICKY_SCROLL_BORDER = registerColor('panelStickyScroll.border', null, localize('panelStickyScrollBorder', "Border color of sticky scroll in the panel.")); -export const PANEL_STICKY_SCROLL_SHADOW = registerColor('panelStickyScroll.shadow', { - dark: scrollbarShadow, - light: scrollbarShadow, - hcDark: scrollbarShadow, - hcLight: scrollbarShadow -}, localize('panelStickyScrollShadow', "Shadow color of sticky scroll in the panel.")); +export const PANEL_STICKY_SCROLL_SHADOW = registerColor('panelStickyScroll.shadow', scrollbarShadow, localize('panelStickyScrollShadow', "Shadow color of sticky scroll in the panel.")); // < --- Output Editor --> -const OUTPUT_VIEW_BACKGROUND = registerColor('outputView.background', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('outputViewBackground', "Output view background color.")); +const OUTPUT_VIEW_BACKGROUND = registerColor('outputView.background', null, localize('outputViewBackground', "Output view background color.")); -registerColor('outputViewStickyScroll.background', { - dark: OUTPUT_VIEW_BACKGROUND, - light: OUTPUT_VIEW_BACKGROUND, - hcDark: OUTPUT_VIEW_BACKGROUND, - hcLight: OUTPUT_VIEW_BACKGROUND -}, localize('outputViewStickyScrollBackground', "Output view sticky scroll background color.")); +registerColor('outputViewStickyScroll.background', OUTPUT_VIEW_BACKGROUND, localize('outputViewStickyScrollBackground', "Output view sticky scroll background color.")); // < --- Banner --- > @@ -481,19 +336,9 @@ export const BANNER_BACKGROUND = registerColor('banner.background', { hcLight: listActiveSelectionBackground }, localize('banner.background', "Banner background color. The banner is shown under the title bar of the window.")); -export const BANNER_FOREGROUND = registerColor('banner.foreground', { - dark: listActiveSelectionForeground, - light: listActiveSelectionForeground, - hcDark: listActiveSelectionForeground, - hcLight: listActiveSelectionForeground -}, localize('banner.foreground', "Banner foreground color. The banner is shown under the title bar of the window.")); +export const BANNER_FOREGROUND = registerColor('banner.foreground', listActiveSelectionForeground, localize('banner.foreground', "Banner foreground color. The banner is shown under the title bar of the window.")); -export const BANNER_ICON_FOREGROUND = registerColor('banner.iconForeground', { - dark: editorInfoForeground, - light: editorInfoForeground, - hcDark: editorInfoForeground, - hcLight: editorInfoForeground -}, localize('banner.iconForeground', "Banner icon color. The banner is shown under the title bar of the window.")); +export const BANNER_ICON_FOREGROUND = registerColor('banner.iconForeground', editorInfoForeground, localize('banner.iconForeground', "Banner icon color. The banner is shown under the title bar of the window.")); // < --- Status --- > @@ -504,12 +349,7 @@ export const STATUS_BAR_FOREGROUND = registerColor('statusBar.foreground', { hcLight: editorForeground }, localize('statusBarForeground', "Status bar foreground color when a workspace or folder is opened. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_NO_FOLDER_FOREGROUND = registerColor('statusBar.noFolderForeground', { - dark: STATUS_BAR_FOREGROUND, - light: STATUS_BAR_FOREGROUND, - hcDark: STATUS_BAR_FOREGROUND, - hcLight: STATUS_BAR_FOREGROUND -}, localize('statusBarNoFolderForeground', "Status bar foreground color when no folder is opened. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_NO_FOLDER_FOREGROUND = registerColor('statusBar.noFolderForeground', STATUS_BAR_FOREGROUND, localize('statusBarNoFolderForeground', "Status bar foreground color when no folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_BACKGROUND = registerColor('statusBar.background', { dark: '#007ACC', @@ -539,12 +379,7 @@ export const STATUS_BAR_FOCUS_BORDER = registerColor('statusBar.focusBorder', { hcLight: STATUS_BAR_FOREGROUND }, localize('statusBarFocusBorder', "Status bar border color when focused on keyboard navigation. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_NO_FOLDER_BORDER = registerColor('statusBar.noFolderBorder', { - dark: STATUS_BAR_BORDER, - light: STATUS_BAR_BORDER, - hcDark: STATUS_BAR_BORDER, - hcLight: STATUS_BAR_BORDER -}, localize('statusBarNoFolderBorder', "Status bar border color separating to the sidebar and editor when no folder is opened. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_NO_FOLDER_BORDER = registerColor('statusBar.noFolderBorder', STATUS_BAR_BORDER, localize('statusBarNoFolderBorder', "Status bar border color separating to the sidebar and editor when no folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_ACTIVE_BACKGROUND = registerColor('statusBarItem.activeBackground', { dark: Color.white.transparent(0.18), @@ -567,12 +402,7 @@ export const STATUS_BAR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.hov hcLight: Color.black.transparent(0.12) }, localize('statusBarItemHoverBackground', "Status bar item background color when hovering. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.hoverForeground', { - dark: STATUS_BAR_FOREGROUND, - light: STATUS_BAR_FOREGROUND, - hcDark: STATUS_BAR_FOREGROUND, - hcLight: STATUS_BAR_FOREGROUND -}, localize('statusBarItemHoverForeground', "Status bar item foreground color when hovering. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.hoverForeground', STATUS_BAR_FOREGROUND, localize('statusBarItemHoverForeground', "Status bar item foreground color when hovering. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND = registerColor('statusBarItem.compactHoverBackground', { dark: Color.white.transparent(0.20), @@ -581,26 +411,11 @@ export const STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND = registerColor('statusBar hcLight: Color.black.transparent(0.20) }, localize('statusBarItemCompactHoverBackground', "Status bar item background color when hovering an item that contains two hovers. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_PROMINENT_ITEM_FOREGROUND = registerColor('statusBarItem.prominentForeground', { - dark: STATUS_BAR_FOREGROUND, - light: STATUS_BAR_FOREGROUND, - hcDark: STATUS_BAR_FOREGROUND, - hcLight: STATUS_BAR_FOREGROUND -}, localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_PROMINENT_ITEM_FOREGROUND = registerColor('statusBarItem.prominentForeground', STATUS_BAR_FOREGROUND, localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_PROMINENT_ITEM_BACKGROUND = registerColor('statusBarItem.prominentBackground', { - dark: Color.black.transparent(0.5), - light: Color.black.transparent(0.5), - hcDark: Color.black.transparent(0.5), - hcLight: Color.black.transparent(0.5), -}, localize('statusBarProminentItemBackground', "Status bar prominent items background color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_PROMINENT_ITEM_BACKGROUND = registerColor('statusBarItem.prominentBackground', Color.black.transparent(0.5), localize('statusBarProminentItemBackground', "Status bar prominent items background color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_PROMINENT_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.prominentHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarProminentItemHoverForeground', "Status bar prominent items foreground color when hovering. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_PROMINENT_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.prominentHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarProminentItemHoverForeground', "Status bar prominent items foreground color when hovering. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.prominentHoverBackground', { dark: Color.black.transparent(0.3), @@ -616,26 +431,11 @@ export const STATUS_BAR_ERROR_ITEM_BACKGROUND = registerColor('statusBarItem.err hcLight: '#B5200D' }, localize('statusBarErrorItemBackground', "Status bar error items background color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ERROR_ITEM_FOREGROUND = registerColor('statusBarItem.errorForeground', { - dark: Color.white, - light: Color.white, - hcDark: Color.white, - hcLight: Color.white -}, localize('statusBarErrorItemForeground', "Status bar error items foreground color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ERROR_ITEM_FOREGROUND = registerColor('statusBarItem.errorForeground', Color.white, localize('statusBarErrorItemForeground', "Status bar error items foreground color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ERROR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.errorHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarErrorItemHoverForeground', "Status bar error items foreground color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ERROR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.errorHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarErrorItemHoverForeground', "Status bar error items foreground color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_ERROR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.errorHoverBackground', { - dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - light: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_BACKGROUND -}, localize('statusBarErrorItemHoverBackground', "Status bar error items background color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_ERROR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.errorHoverBackground', STATUS_BAR_ITEM_HOVER_BACKGROUND, localize('statusBarErrorItemHoverBackground', "Status bar error items background color when hovering. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_WARNING_ITEM_BACKGROUND = registerColor('statusBarItem.warningBackground', { dark: darken(editorWarningForeground, .4), @@ -644,26 +444,11 @@ export const STATUS_BAR_WARNING_ITEM_BACKGROUND = registerColor('statusBarItem.w hcLight: '#895503' }, localize('statusBarWarningItemBackground', "Status bar warning items background color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_WARNING_ITEM_FOREGROUND = registerColor('statusBarItem.warningForeground', { - dark: Color.white, - light: Color.white, - hcDark: Color.white, - hcLight: Color.white -}, localize('statusBarWarningItemForeground', "Status bar warning items foreground color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_WARNING_ITEM_FOREGROUND = registerColor('statusBarItem.warningForeground', Color.white, localize('statusBarWarningItemForeground', "Status bar warning items foreground color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_WARNING_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.warningHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarWarningItemHoverForeground', "Status bar warning items foreground color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_WARNING_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.warningHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarWarningItemHoverForeground', "Status bar warning items foreground color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); -export const STATUS_BAR_WARNING_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.warningHoverBackground', { - dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - light: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_BACKGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_BACKGROUND -}, localize('statusBarWarningItemHoverBackground', "Status bar warning items background color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_WARNING_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.warningHoverBackground', STATUS_BAR_ITEM_HOVER_BACKGROUND, localize('statusBarWarningItemHoverBackground', "Status bar warning items background color when hovering. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); // < --- Activity Bar --- > @@ -710,12 +495,7 @@ export const ACTIVITY_BAR_ACTIVE_FOCUS_BORDER = registerColor('activityBar.activ hcLight: '#B5200D' }, localize('activityBarActiveFocusBorder', "Activity bar focus border color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_ACTIVE_BACKGROUND = registerColor('activityBar.activeBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('activityBarActiveBackground', "Activity bar background color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_ACTIVE_BACKGROUND = registerColor('activityBar.activeBackground', null, localize('activityBarActiveBackground', "Activity bar background color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_DRAG_AND_DROP_BORDER = registerColor('activityBar.dropBorder', { dark: ACTIVITY_BAR_FOREGROUND, @@ -731,12 +511,7 @@ export const ACTIVITY_BAR_BADGE_BACKGROUND = registerColor('activityBarBadge.bac hcLight: '#0F4A85' }, localize('activityBarBadgeBackground', "Activity notification badge background color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.foreground', { - dark: Color.white, - light: Color.white, - hcDark: Color.white, - hcLight: Color.white -}, localize('activityBarBadgeForeground', "Activity notification badge foreground color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.foreground', Color.white, localize('activityBarBadgeForeground', "Activity notification badge foreground color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_FOREGROUND = registerColor('activityBarTop.foreground', { dark: '#E7E7E7', @@ -752,12 +527,7 @@ export const ACTIVITY_BAR_TOP_ACTIVE_BORDER = registerColor('activityBarTop.acti hcLight: '#B5200D' }, localize('activityBarTopActiveFocusBorder', "Focus border color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_TOP_ACTIVE_BACKGROUND = registerColor('activityBarTop.activeBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('activityBarTopActiveBackground', "Background color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_TOP_ACTIVE_BACKGROUND = registerColor('activityBarTop.activeBackground', null, localize('activityBarTopActiveBackground', "Background color for the active item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); export const ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND = registerColor('activityBarTop.inactiveForeground', { dark: transparent(ACTIVITY_BAR_TOP_FOREGROUND, 0.6), @@ -766,19 +536,9 @@ export const ACTIVITY_BAR_TOP_INACTIVE_FOREGROUND = registerColor('activityBarTo hcLight: editorForeground }, localize('activityBarTopInActiveForeground', "Inactive foreground color of the item in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER = registerColor('activityBarTop.dropBorder', { - dark: ACTIVITY_BAR_TOP_FOREGROUND, - light: ACTIVITY_BAR_TOP_FOREGROUND, - hcDark: ACTIVITY_BAR_TOP_FOREGROUND, - hcLight: ACTIVITY_BAR_TOP_FOREGROUND -}, localize('activityBarTopDragAndDropBorder', "Drag and drop feedback color for the items in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_TOP_DRAG_AND_DROP_BORDER = registerColor('activityBarTop.dropBorder', ACTIVITY_BAR_TOP_FOREGROUND, localize('activityBarTopDragAndDropBorder', "Drag and drop feedback color for the items in the Activity bar when it is on top / bottom. The activity allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_TOP_BACKGROUND = registerColor('activityBarTop.background', { - dark: null, - light: null, - hcDark: null, - hcLight: null, -}, localize('activityBarTopBackground', "Background color of the activity bar when set to top / bottom.")); +export const ACTIVITY_BAR_TOP_BACKGROUND = registerColor('activityBarTop.background', null, localize('activityBarTopBackground', "Background color of the activity bar when set to top / bottom.")); // < --- Profiles --- > @@ -799,26 +559,11 @@ export const PROFILE_BADGE_FOREGROUND = registerColor('profileBadge.foreground', // < --- Remote --- > -export const STATUS_BAR_REMOTE_ITEM_BACKGROUND = registerColor('statusBarItem.remoteBackground', { - dark: ACTIVITY_BAR_BADGE_BACKGROUND, - light: ACTIVITY_BAR_BADGE_BACKGROUND, - hcDark: ACTIVITY_BAR_BADGE_BACKGROUND, - hcLight: ACTIVITY_BAR_BADGE_BACKGROUND -}, localize('statusBarItemHostBackground', "Background color for the remote indicator on the status bar.")); +export const STATUS_BAR_REMOTE_ITEM_BACKGROUND = registerColor('statusBarItem.remoteBackground', ACTIVITY_BAR_BADGE_BACKGROUND, localize('statusBarItemHostBackground', "Background color for the remote indicator on the status bar.")); -export const STATUS_BAR_REMOTE_ITEM_FOREGROUND = registerColor('statusBarItem.remoteForeground', { - dark: ACTIVITY_BAR_BADGE_FOREGROUND, - light: ACTIVITY_BAR_BADGE_FOREGROUND, - hcDark: ACTIVITY_BAR_BADGE_FOREGROUND, - hcLight: ACTIVITY_BAR_BADGE_FOREGROUND -}, localize('statusBarItemHostForeground', "Foreground color for the remote indicator on the status bar.")); +export const STATUS_BAR_REMOTE_ITEM_FOREGROUND = registerColor('statusBarItem.remoteForeground', ACTIVITY_BAR_BADGE_FOREGROUND, localize('statusBarItemHostForeground', "Foreground color for the remote indicator on the status bar.")); -export const STATUS_BAR_REMOTE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.remoteHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarRemoteItemHoverForeground', "Foreground color for the remote indicator on the status bar when hovering.")); +export const STATUS_BAR_REMOTE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.remoteHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarRemoteItemHoverForeground', "Foreground color for the remote indicator on the status bar when hovering.")); export const STATUS_BAR_REMOTE_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.remoteHoverBackground', { dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, @@ -827,26 +572,11 @@ export const STATUS_BAR_REMOTE_ITEM_HOVER_BACKGROUND = registerColor('statusBarI hcLight: null }, localize('statusBarRemoteItemHoverBackground', "Background color for the remote indicator on the status bar when hovering.")); -export const STATUS_BAR_OFFLINE_ITEM_BACKGROUND = registerColor('statusBarItem.offlineBackground', { - dark: '#6c1717', - light: '#6c1717', - hcDark: '#6c1717', - hcLight: '#6c1717' -}, localize('statusBarItemOfflineBackground', "Status bar item background color when the workbench is offline.")); +export const STATUS_BAR_OFFLINE_ITEM_BACKGROUND = registerColor('statusBarItem.offlineBackground', '#6c1717', localize('statusBarItemOfflineBackground', "Status bar item background color when the workbench is offline.")); -export const STATUS_BAR_OFFLINE_ITEM_FOREGROUND = registerColor('statusBarItem.offlineForeground', { - dark: STATUS_BAR_REMOTE_ITEM_FOREGROUND, - light: STATUS_BAR_REMOTE_ITEM_FOREGROUND, - hcDark: STATUS_BAR_REMOTE_ITEM_FOREGROUND, - hcLight: STATUS_BAR_REMOTE_ITEM_FOREGROUND -}, localize('statusBarItemOfflineForeground', "Status bar item foreground color when the workbench is offline.")); +export const STATUS_BAR_OFFLINE_ITEM_FOREGROUND = registerColor('statusBarItem.offlineForeground', STATUS_BAR_REMOTE_ITEM_FOREGROUND, localize('statusBarItemOfflineForeground', "Status bar item foreground color when the workbench is offline.")); -export const STATUS_BAR_OFFLINE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.offlineHoverForeground', { - dark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - light: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcDark: STATUS_BAR_ITEM_HOVER_FOREGROUND, - hcLight: STATUS_BAR_ITEM_HOVER_FOREGROUND -}, localize('statusBarOfflineItemHoverForeground', "Status bar item foreground hover color when the workbench is offline.")); +export const STATUS_BAR_OFFLINE_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.offlineHoverForeground', STATUS_BAR_ITEM_HOVER_FOREGROUND, localize('statusBarOfflineItemHoverForeground', "Status bar item foreground hover color when the workbench is offline.")); export const STATUS_BAR_OFFLINE_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.offlineHoverBackground', { dark: STATUS_BAR_ITEM_HOVER_BACKGROUND, @@ -855,19 +585,9 @@ export const STATUS_BAR_OFFLINE_ITEM_HOVER_BACKGROUND = registerColor('statusBar hcLight: null }, localize('statusBarOfflineItemHoverBackground', "Status bar item background hover color when the workbench is offline.")); -export const EXTENSION_BADGE_REMOTE_BACKGROUND = registerColor('extensionBadge.remoteBackground', { - dark: ACTIVITY_BAR_BADGE_BACKGROUND, - light: ACTIVITY_BAR_BADGE_BACKGROUND, - hcDark: ACTIVITY_BAR_BADGE_BACKGROUND, - hcLight: ACTIVITY_BAR_BADGE_BACKGROUND -}, localize('extensionBadge.remoteBackground', "Background color for the remote badge in the extensions view.")); +export const EXTENSION_BADGE_REMOTE_BACKGROUND = registerColor('extensionBadge.remoteBackground', ACTIVITY_BAR_BADGE_BACKGROUND, localize('extensionBadge.remoteBackground', "Background color for the remote badge in the extensions view.")); -export const EXTENSION_BADGE_REMOTE_FOREGROUND = registerColor('extensionBadge.remoteForeground', { - dark: ACTIVITY_BAR_BADGE_FOREGROUND, - light: ACTIVITY_BAR_BADGE_FOREGROUND, - hcDark: ACTIVITY_BAR_BADGE_FOREGROUND, - hcLight: ACTIVITY_BAR_BADGE_FOREGROUND -}, localize('extensionBadge.remoteForeground', "Foreground color for the remote badge in the extensions view.")); +export const EXTENSION_BADGE_REMOTE_FOREGROUND = registerColor('extensionBadge.remoteForeground', ACTIVITY_BAR_BADGE_FOREGROUND, localize('extensionBadge.remoteForeground', "Foreground color for the remote badge in the extensions view.")); // < --- Side Bar --- > @@ -879,12 +599,7 @@ export const SIDE_BAR_BACKGROUND = registerColor('sideBar.background', { hcLight: '#FFFFFF' }, localize('sideBarBackground', "Side bar background color. The side bar is the container for views like explorer and search.")); -export const SIDE_BAR_FOREGROUND = registerColor('sideBar.foreground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('sideBarForeground', "Side bar foreground color. The side bar is the container for views like explorer and search.")); +export const SIDE_BAR_FOREGROUND = registerColor('sideBar.foreground', null, localize('sideBarForeground', "Side bar foreground color. The side bar is the container for views like explorer and search.")); export const SIDE_BAR_BORDER = registerColor('sideBar.border', { dark: null, @@ -893,26 +608,11 @@ export const SIDE_BAR_BORDER = registerColor('sideBar.border', { hcLight: contrastBorder }, localize('sideBarBorder', "Side bar border color on the side separating to the editor. The side bar is the container for views like explorer and search.")); -export const SIDE_BAR_TITLE_BACKGROUND = registerColor('sideBarTitle.background', { - dark: SIDE_BAR_BACKGROUND, - light: SIDE_BAR_BACKGROUND, - hcDark: SIDE_BAR_BACKGROUND, - hcLight: SIDE_BAR_BACKGROUND -}, localize('sideBarTitleBackground', "Side bar title background color. The side bar is the container for views like explorer and search.")); +export const SIDE_BAR_TITLE_BACKGROUND = registerColor('sideBarTitle.background', SIDE_BAR_BACKGROUND, localize('sideBarTitleBackground', "Side bar title background color. The side bar is the container for views like explorer and search.")); -export const SIDE_BAR_TITLE_FOREGROUND = registerColor('sideBarTitle.foreground', { - dark: SIDE_BAR_FOREGROUND, - light: SIDE_BAR_FOREGROUND, - hcDark: SIDE_BAR_FOREGROUND, - hcLight: SIDE_BAR_FOREGROUND -}, localize('sideBarTitleForeground', "Side bar title foreground color. The side bar is the container for views like explorer and search.")); +export const SIDE_BAR_TITLE_FOREGROUND = registerColor('sideBarTitle.foreground', SIDE_BAR_FOREGROUND, localize('sideBarTitleForeground', "Side bar title foreground color. The side bar is the container for views like explorer and search.")); -export const SIDE_BAR_DRAG_AND_DROP_BACKGROUND = registerColor('sideBar.dropBackground', { - dark: EDITOR_DRAG_AND_DROP_BACKGROUND, - light: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcDark: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcLight: EDITOR_DRAG_AND_DROP_BACKGROUND -}, localize('sideBarDragAndDropBackground', "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); +export const SIDE_BAR_DRAG_AND_DROP_BACKGROUND = registerColor('sideBar.dropBackground', EDITOR_DRAG_AND_DROP_BACKGROUND, localize('sideBarDragAndDropBackground', "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_BACKGROUND = registerColor('sideBarSectionHeader.background', { dark: Color.fromHex('#808080').transparent(0.2), @@ -921,47 +621,17 @@ export const SIDE_BAR_SECTION_HEADER_BACKGROUND = registerColor('sideBarSectionH hcLight: null }, localize('sideBarSectionHeaderBackground', "Side bar section header background color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); -export const SIDE_BAR_SECTION_HEADER_FOREGROUND = registerColor('sideBarSectionHeader.foreground', { - dark: SIDE_BAR_FOREGROUND, - light: SIDE_BAR_FOREGROUND, - hcDark: SIDE_BAR_FOREGROUND, - hcLight: SIDE_BAR_FOREGROUND -}, localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); +export const SIDE_BAR_SECTION_HEADER_FOREGROUND = registerColor('sideBarSectionHeader.foreground', SIDE_BAR_FOREGROUND, localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); -export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeader.border', { - dark: contrastBorder, - light: contrastBorder, - hcDark: contrastBorder, - hcLight: contrastBorder -}, localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); +export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeader.border', contrastBorder, localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); -export const ACTIVITY_BAR_TOP_BORDER = registerColor('sideBarActivityBarTop.border', { - dark: SIDE_BAR_SECTION_HEADER_BORDER, - light: SIDE_BAR_SECTION_HEADER_BORDER, - hcDark: SIDE_BAR_SECTION_HEADER_BORDER, - hcLight: SIDE_BAR_SECTION_HEADER_BORDER -}, localize('sideBarActivityBarTopBorder', "Border color between the activity bar at the top/bottom and the views.")); +export const ACTIVITY_BAR_TOP_BORDER = registerColor('sideBarActivityBarTop.border', SIDE_BAR_SECTION_HEADER_BORDER, localize('sideBarActivityBarTopBorder', "Border color between the activity bar at the top/bottom and the views.")); -export const SIDE_BAR_STICKY_SCROLL_BACKGROUND = registerColor('sideBarStickyScroll.background', { - dark: SIDE_BAR_BACKGROUND, - light: SIDE_BAR_BACKGROUND, - hcDark: SIDE_BAR_BACKGROUND, - hcLight: SIDE_BAR_BACKGROUND -}, localize('sideBarStickyScrollBackground', "Background color of sticky scroll in the side bar.")); +export const SIDE_BAR_STICKY_SCROLL_BACKGROUND = registerColor('sideBarStickyScroll.background', SIDE_BAR_BACKGROUND, localize('sideBarStickyScrollBackground', "Background color of sticky scroll in the side bar.")); -export const SIDE_BAR_STICKY_SCROLL_BORDER = registerColor('sideBarStickyScroll.border', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('sideBarStickyScrollBorder', "Border color of sticky scroll in the side bar.")); +export const SIDE_BAR_STICKY_SCROLL_BORDER = registerColor('sideBarStickyScroll.border', null, localize('sideBarStickyScrollBorder', "Border color of sticky scroll in the side bar.")); -export const SIDE_BAR_STICKY_SCROLL_SHADOW = registerColor('sideBarStickyScroll.shadow', { - dark: scrollbarShadow, - light: scrollbarShadow, - hcDark: scrollbarShadow, - hcLight: scrollbarShadow -}, localize('sideBarStickyScrollShadow', "Shadow color of sticky scroll in the side bar.")); +export const SIDE_BAR_STICKY_SCROLL_SHADOW = registerColor('sideBarStickyScroll.shadow', scrollbarShadow, localize('sideBarStickyScrollShadow', "Shadow color of sticky scroll in the side bar.")); // < --- Title Bar --- > @@ -1002,12 +672,7 @@ export const TITLE_BAR_BORDER = registerColor('titleBar.border', { // < --- Menubar --- > -export const MENUBAR_SELECTION_FOREGROUND = registerColor('menubar.selectionForeground', { - dark: TITLE_BAR_ACTIVE_FOREGROUND, - light: TITLE_BAR_ACTIVE_FOREGROUND, - hcDark: TITLE_BAR_ACTIVE_FOREGROUND, - hcLight: TITLE_BAR_ACTIVE_FOREGROUND, -}, localize('menubarSelectionForeground', "Foreground color of the selected menu item in the menubar.")); +export const MENUBAR_SELECTION_FOREGROUND = registerColor('menubar.selectionForeground', TITLE_BAR_ACTIVE_FOREGROUND, localize('menubarSelectionForeground', "Foreground color of the selected menu item in the menubar.")); export const MENUBAR_SELECTION_BACKGROUND = registerColor('menubar.selectionBackground', { dark: toolbarHoverBackground, @@ -1028,19 +693,19 @@ export const MENUBAR_SELECTION_BORDER = registerColor('menubar.selectionBorder', // foreground (inactive and active) export const COMMAND_CENTER_FOREGROUND = registerColor( 'commandCenter.foreground', - { dark: TITLE_BAR_ACTIVE_FOREGROUND, hcDark: TITLE_BAR_ACTIVE_FOREGROUND, light: TITLE_BAR_ACTIVE_FOREGROUND, hcLight: TITLE_BAR_ACTIVE_FOREGROUND }, + TITLE_BAR_ACTIVE_FOREGROUND, localize('commandCenter-foreground', "Foreground color of the command center"), false ); export const COMMAND_CENTER_ACTIVEFOREGROUND = registerColor( 'commandCenter.activeForeground', - { dark: MENUBAR_SELECTION_FOREGROUND, hcDark: MENUBAR_SELECTION_FOREGROUND, light: MENUBAR_SELECTION_FOREGROUND, hcLight: MENUBAR_SELECTION_FOREGROUND }, + MENUBAR_SELECTION_FOREGROUND, localize('commandCenter-activeForeground', "Active foreground color of the command center"), false ); export const COMMAND_CENTER_INACTIVEFOREGROUND = registerColor( 'commandCenter.inactiveForeground', - { dark: TITLE_BAR_INACTIVE_FOREGROUND, hcDark: TITLE_BAR_INACTIVE_FOREGROUND, light: TITLE_BAR_INACTIVE_FOREGROUND, hcLight: TITLE_BAR_INACTIVE_FOREGROUND }, + TITLE_BAR_INACTIVE_FOREGROUND, localize('commandCenter-inactiveForeground', "Foreground color of the command center when the window is inactive"), false ); @@ -1070,7 +735,7 @@ export const COMMAND_CENTER_ACTIVEBORDER = registerColor( ); // border: defaults to active background export const COMMAND_CENTER_INACTIVEBORDER = registerColor( - 'commandCenter.inactiveBorder', { dark: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), hcDark: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), light: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), hcLight: transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25) }, + 'commandCenter.inactiveBorder', transparent(TITLE_BAR_INACTIVE_FOREGROUND, .25), localize('commandCenter-inactiveBorder', "Border color of the command center when the window is inactive"), false ); @@ -1092,33 +757,13 @@ export const NOTIFICATIONS_TOAST_BORDER = registerColor('notificationToast.borde hcLight: contrastBorder }, localize('notificationToastBorder', "Notification toast border color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_FOREGROUND = registerColor('notifications.foreground', { - dark: editorWidgetForeground, - light: editorWidgetForeground, - hcDark: editorWidgetForeground, - hcLight: editorWidgetForeground -}, localize('notificationsForeground', "Notifications foreground color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_FOREGROUND = registerColor('notifications.foreground', editorWidgetForeground, localize('notificationsForeground', "Notifications foreground color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_BACKGROUND = registerColor('notifications.background', { - dark: editorWidgetBackground, - light: editorWidgetBackground, - hcDark: editorWidgetBackground, - hcLight: editorWidgetBackground -}, localize('notificationsBackground', "Notifications background color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_BACKGROUND = registerColor('notifications.background', editorWidgetBackground, localize('notificationsBackground', "Notifications background color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_LINKS = registerColor('notificationLink.foreground', { - dark: textLinkForeground, - light: textLinkForeground, - hcDark: textLinkForeground, - hcLight: textLinkForeground -}, localize('notificationsLink', "Notification links foreground color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_LINKS = registerColor('notificationLink.foreground', textLinkForeground, localize('notificationsLink', "Notification links foreground color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_CENTER_HEADER_FOREGROUND = registerColor('notificationCenterHeader.foreground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('notificationCenterHeaderForeground', "Notifications center header foreground color. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_CENTER_HEADER_FOREGROUND = registerColor('notificationCenterHeader.foreground', null, localize('notificationCenterHeaderForeground', "Notifications center header foreground color. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_CENTER_HEADER_BACKGROUND = registerColor('notificationCenterHeader.background', { dark: lighten(NOTIFICATIONS_BACKGROUND, 0.3), @@ -1127,33 +772,13 @@ export const NOTIFICATIONS_CENTER_HEADER_BACKGROUND = registerColor('notificatio hcLight: NOTIFICATIONS_BACKGROUND }, localize('notificationCenterHeaderBackground', "Notifications center header background color. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_BORDER = registerColor('notifications.border', { - dark: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, - light: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, - hcDark: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, - hcLight: NOTIFICATIONS_CENTER_HEADER_BACKGROUND -}, localize('notificationsBorder', "Notifications border color separating from other notifications in the notifications center. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_BORDER = registerColor('notifications.border', NOTIFICATIONS_CENTER_HEADER_BACKGROUND, localize('notificationsBorder', "Notifications border color separating from other notifications in the notifications center. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_ERROR_ICON_FOREGROUND = registerColor('notificationsErrorIcon.foreground', { - dark: editorErrorForeground, - light: editorErrorForeground, - hcDark: editorErrorForeground, - hcLight: editorErrorForeground -}, localize('notificationsErrorIconForeground', "The color used for the icon of error notifications. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_ERROR_ICON_FOREGROUND = registerColor('notificationsErrorIcon.foreground', editorErrorForeground, localize('notificationsErrorIconForeground', "The color used for the icon of error notifications. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_WARNING_ICON_FOREGROUND = registerColor('notificationsWarningIcon.foreground', { - dark: editorWarningForeground, - light: editorWarningForeground, - hcDark: editorWarningForeground, - hcLight: editorWarningForeground -}, localize('notificationsWarningIconForeground', "The color used for the icon of warning notifications. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_WARNING_ICON_FOREGROUND = registerColor('notificationsWarningIcon.foreground', editorWarningForeground, localize('notificationsWarningIconForeground', "The color used for the icon of warning notifications. Notifications slide in from the bottom right of the window.")); -export const NOTIFICATIONS_INFO_ICON_FOREGROUND = registerColor('notificationsInfoIcon.foreground', { - dark: editorInfoForeground, - light: editorInfoForeground, - hcDark: editorInfoForeground, - hcLight: editorInfoForeground -}, localize('notificationsInfoIconForeground', "The color used for the icon of info notifications. Notifications slide in from the bottom right of the window.")); +export const NOTIFICATIONS_INFO_ICON_FOREGROUND = registerColor('notificationsInfoIcon.foreground', editorInfoForeground, localize('notificationsInfoIconForeground', "The color used for the icon of info notifications. Notifications slide in from the bottom right of the window.")); export const WINDOW_ACTIVE_BORDER = registerColor('window.activeBorder', { dark: null, diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 453b43eb916..fa4e47467d2 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -56,6 +56,7 @@ export const enum AccessibilityVerbositySettingId { Hover = 'accessibility.verbosity.hover', Notification = 'accessibility.verbosity.notification', EmptyEditorHint = 'accessibility.verbosity.emptyEditorHint', + ReplInputHint = 'accessibility.verbosity.replInputHint', Comments = 'accessibility.verbosity.comments', DiffEditorActive = 'accessibility.verbosity.diffEditorActive' } @@ -158,6 +159,10 @@ const configuration: IConfigurationNode = { description: localize('verbosity.emptyEditorHint', 'Provide information about relevant actions in an empty text editor.'), ...baseVerbosityProperty }, + [AccessibilityVerbositySettingId.ReplInputHint]: { + description: localize('verbosity.replInputHint', 'Provide information about relevant actions For the Repl input.'), + ...baseVerbosityProperty + }, [AccessibilityVerbositySettingId.Comments]: { description: localize('verbosity.comments', 'Provide information about actions that can be taken in the comment widget or in a file which contains comments.'), ...baseVerbosityProperty @@ -171,96 +176,77 @@ const configuration: IConfigurationNode = { type: 'boolean', default: true }, - 'accessibility.signalOptions': { - type: 'object', - additionalProperties: false, - properties: { - 'volume': { - 'description': localize('accessibility.signalOptions.volume', "The volume of the sounds in percent (0-100)."), + 'accessibility.signalOptions.volume': { + 'description': localize('accessibility.signalOptions.volume', "The volume of the sounds in percent (0-100)."), + 'type': 'number', + 'minimum': 0, + 'maximum': 100, + 'default': 70, + 'tags': ['accessibility'] + }, + 'accessibility.signalOptions.debouncePositionChanges': { + 'description': localize('accessibility.signalOptions.debouncePositionChanges', "Whether or not position changes should be debounced"), + 'type': 'boolean', + 'default': false, + 'tags': ['accessibility'] + }, + 'accessibility.signalOptions.experimental.delays.general': { + 'type': 'object', + 'description': 'Delays for all signals besides error and warning at position', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.general.announcement', "The delay in milliseconds before an announcement is made."), 'type': 'number', 'minimum': 0, - 'maximum': 100, - 'default': 70, + 'default': 3000 }, - 'debouncePositionChanges': { - 'description': localize('accessibility.signalOptions.debouncePositionChanges', "Whether or not position changes should be debounced"), - 'type': 'boolean', - 'default': false, - }, - 'delays': { - 'type': 'object', - 'additionalProperties': false, - 'properties': { - 'general': { - 'type': 'object', - 'additionalProperties': false, - 'properties': { - 'announcement': { - 'description': localize('accessibility.signalOptions.delays.general.announcement', "The delay in milliseconds before an announcement is made."), - 'type': 'number', - 'minimum': 0, - }, - 'sound': { - 'description': localize('accessibility.signalOptions.delays.general.sound', "The delay in milliseconds before a sound is played."), - 'type': 'number', - 'minimum': 0, - } - }, - }, - 'warningAtPosition': { - 'type': 'object', - 'additionalProperties': false, - 'properties': { - 'announcement': { - 'description': localize('accessibility.signalOptions.delays.warningAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's a warning at the position."), - 'type': 'number', - 'minimum': 0, - }, - 'sound': { - 'description': localize('accessibility.signalOptions.delays.warningAtPosition.sound', "The delay in milliseconds before a sound is played when there's a warning at the position."), - 'type': 'number', - 'minimum': 0, - } - }, - }, - 'errorAtPosition': { - 'type': 'object', - 'additionalProperties': false, - 'properties': { - 'announcement': { - 'description': localize('accessibility.signalOptions.delays.errorAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's an error at the position."), - 'type': 'number', - 'minimum': 0, - }, - 'sound': { - 'description': localize('accessibility.signalOptions.delays.errorAtPosition.sound', "The delay in milliseconds before a sound is played when there's an error at the position."), - 'type': 'number', - 'minimum': 0, - } - }, - }, - } + 'sound': { + 'description': localize('accessibility.signalOptions.delays.general.sound', "The delay in milliseconds before a sound is played."), + 'type': 'number', + 'minimum': 0, + 'default': 400 } }, - default: { - 'volume': 70, - 'debouncePositionChanges': false, - 'delays': { - 'general': { - 'announcement': 3000, - 'sound': 400 - }, - 'warningAtPosition': { - 'announcement': 3000, - 'sound': 1000 - }, - 'errorAtPosition': { - 'announcement': 3000, - 'sound': 1000 - } + 'tags': ['accessibility'] + }, + 'accessibility.signalOptions.experimental.delays.warningAtPosition': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.warningAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's a warning at the position."), + 'type': 'number', + 'minimum': 0, + 'default': 3000 + }, + 'sound': { + 'description': localize('accessibility.signalOptions.delays.warningAtPosition.sound', "The delay in milliseconds before a sound is played when there's a warning at the position."), + 'type': 'number', + 'minimum': 0, + 'default': 1000 } }, - tags: ['accessibility'] + 'tags': ['accessibility'] + }, + 'accessibility.signalOptions.experimental.delays.errorAtPosition': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.errorAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's an error at the position."), + 'type': 'number', + 'minimum': 0, + 'default': 3000 + }, + 'sound': { + 'description': localize('accessibility.signalOptions.delays.errorAtPosition.sound', "The delay in milliseconds before a sound is played when there's an error at the position."), + 'type': 'number', + 'minimum': 0, + 'default': 1000 + } + }, + 'tags': ['accessibility'] }, 'accessibility.signals.lineHasBreakpoint': { ...signalFeatureBase, @@ -676,6 +662,11 @@ const configuration: IConfigurationNode = { 'announcement': 'never' } }, + 'accessibility.underlineLinks': { + 'type': 'boolean', + 'description': localize('accessibility.underlineLinks', "Controls whether links should be underlined in the workbench."), + 'default': false, + }, } }; @@ -712,7 +703,7 @@ export function registerAccessibilityConfiguration() { }); } -export { AccessibilityVoiceSettingId } +export { AccessibilityVoiceSettingId }; export const SpeechTimeoutDefault = 1200; @@ -782,10 +773,9 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'audioCues.volume', - migrateFn: (volume, accessor) => { - const debouncePositionChanges = getDebouncePositionChangesFromConfig(accessor); + migrateFn: (value, accessor) => { return [ - ['accessibility.signalOptions', { value: debouncePositionChanges !== undefined ? { volume, debouncePositionChanges } : { volume } }], + ['accessibility.signalOptions.volume', { value }], ['audioCues.volume', { value: undefined }] ]; } @@ -794,10 +784,9 @@ Registry.as(WorkbenchExtensions.ConfigurationMi Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'audioCues.debouncePositionChanges', - migrateFn: (debouncePositionChanges, accessor) => { - const volume = getVolumeFromConfig(accessor); + migrateFn: (value) => { return [ - ['accessibility.signalOptions', { value: volume !== undefined ? { volume, debouncePositionChanges } : { debouncePositionChanges } }], + ['accessibility.signalOptions.debouncePositionChanges', { value }], ['audioCues.debouncePositionChanges', { value: undefined }] ]; } @@ -805,11 +794,31 @@ Registry.as(WorkbenchExtensions.ConfigurationMi Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ - key: 'accessibility.signals.sounds.volume', - migrateFn: (volume, accessor) => { + key: 'accessibility.signalOptions', + migrateFn: (value, accessor) => { + const delayGeneral = getDelaysFromConfig(accessor, 'general'); + const delayError = getDelaysFromConfig(accessor, 'errorAtPosition'); + const delayWarning = getDelaysFromConfig(accessor, 'warningAtPosition'); + const volume = getVolumeFromConfig(accessor); const debouncePositionChanges = getDebouncePositionChangesFromConfig(accessor); return [ - ['accessibility.signalOptions', { value: debouncePositionChanges !== undefined ? { volume, debouncePositionChanges } : { volume } }], + ['accessibility.signalOptions.volume', { value: volume }], + ['accessibility.signalOptions.debouncePositionChanges', { value: debouncePositionChanges }], + ['accessibility.signalOptions.experimental.delays.general', { value: delayGeneral }], + ['accessibility.signalOptions.experimental.delays.errorAtPosition', { value: delayError }], + ['accessibility.signalOptions.experimental.delays.warningAtPosition', { value: delayWarning }], + ['accessibility.signalOptions', { value: undefined }], + ]; + } + }]); + + +Registry.as(WorkbenchExtensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: 'accessibility.signals.sounds.volume', + migrateFn: (value) => { + return [ + ['accessibility.signalOptions.volume', { value }], ['accessibility.signals.sounds.volume', { value: undefined }] ]; } @@ -818,21 +827,24 @@ Registry.as(WorkbenchExtensions.ConfigurationMi Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'accessibility.signals.debouncePositionChanges', - migrateFn: (debouncePositionChanges, accessor) => { - const volume = getVolumeFromConfig(accessor); + migrateFn: (value) => { return [ - ['accessibility.signalOptions', { value: volume !== undefined ? { volume, debouncePositionChanges } : { debouncePositionChanges } }], + ['accessibility.signalOptions.debouncePositionChanges', { value }], ['accessibility.signals.debouncePositionChanges', { value: undefined }] ]; } }]); +function getDelaysFromConfig(accessor: (key: string) => any, type: 'general' | 'errorAtPosition' | 'warningAtPosition'): { announcement: number; sound: number } | undefined { + return accessor(`accessibility.signalOptions.experimental.delays.${type}`) || accessor('accessibility.signalOptions')?.['experimental.delays']?.[`${type}`] || accessor('accessibility.signalOptions')?.['delays']?.[`${type}`]; +} + function getVolumeFromConfig(accessor: (key: string) => any): string | undefined { - return accessor('accessibility.signalOptions')?.volume || accessor('accessibility.signals.sounds.volume') || accessor('audioCues.volume'); + return accessor('accessibility.signalOptions.volume') || accessor('accessibility.signalOptions')?.volume || accessor('accessibility.signals.sounds.volume') || accessor('audioCues.volume'); } function getDebouncePositionChangesFromConfig(accessor: (key: string) => any): number | undefined { - return accessor('accessibility.signalOptions')?.debouncePositionChanges || accessor('accessibility.signals.debouncePositionChanges') || accessor('audioCues.debouncePositionChanges'); + return accessor('accessibility.signalOptions.debouncePositionChanges') || accessor('accessibility.signalOptions')?.debouncePositionChanges || accessor('accessibility.signals.debouncePositionChanges') || accessor('audioCues.debouncePositionChanges'); } Registry.as(WorkbenchExtensions.ConfigurationMigration) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index d1a40cc9446..d4bd1f893e8 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -73,6 +73,7 @@ export class AccessibleView extends Disposable { private _accessibleViewInCodeBlock: IContextKey; private _accessibleViewContainsCodeBlocks: IContextKey; private _codeBlocks?: ICodeBlock[]; + private _inQuickPick: boolean = false; get editorWidget() { return this._editorWidget; } private _container: HTMLElement; @@ -245,10 +246,13 @@ export class AccessibleView extends Disposable { if (!provider) { return; } + provider.onOpen?.(); + let viewContainer: HTMLElement | undefined; const delegate: IContextViewDelegate = { getAnchor: () => { return { x: (getActiveWindow().innerWidth / 2) - ((Math.min(this._layoutService.activeContainerDimension.width * 0.62 /* golden cut */, DIMENSIONS.MAX_WIDTH)) / 2), y: this._layoutService.activeContainerOffset.quickPickTop }; }, render: (container) => { - container.classList.add('accessible-view-container'); + viewContainer = container; + viewContainer.classList.add('accessible-view-container'); return this._render(provider, container, showAccessibleViewHelp); }, onHide: () => { @@ -289,6 +293,11 @@ export class AccessibleView extends Disposable { if (provider instanceof ExtensionContentProvider) { this._storageService.store(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${provider.id}`, true, StorageScope.APPLICATION, StorageTarget.USER); } + if (provider.onDidChangeContent) { + this._register(provider.onDidChangeContent(() => { + if (viewContainer) { this._render(provider, viewContainer, showAccessibleViewHelp); } + })); + } } previous(): void { @@ -371,8 +380,9 @@ export class AccessibleView extends Disposable { } configureKeybindings(): void { - const items = this._currentProvider?.options?.configureKeybindingItems; - const provider = this._currentProvider; + this._inQuickPick = true; + const provider = this._updateLastProvider(); + const items = provider?.options?.configureKeybindingItems; if (!items) { return; } @@ -394,6 +404,7 @@ export class AccessibleView extends Disposable { this.show(provider); } quickPick.dispose(); + this._inQuickPick = false; }); } @@ -559,9 +570,11 @@ export class AccessibleView extends Disposable { }); this._updateToolbar(this._currentProvider.actions, provider.options.type); - const hide = (e: KeyboardEvent | IKeyboardEvent): void => { - provider.onClose(); - e.stopPropagation(); + const hide = (e?: KeyboardEvent | IKeyboardEvent): void => { + if (!this._inQuickPick) { + provider.onClose(); + } + e?.stopPropagation(); this._contextViewService.hideContextView(); this._updateContextKeys(provider, false); this._lastProvider = undefined; @@ -592,7 +605,7 @@ export class AccessibleView extends Disposable { })); disposableStore.add(this._editorWidget.onDidBlurEditorWidget(() => { if (!isActiveElement(this._toolbar.getElement())) { - this._contextViewService.hideContextView(); + hide(); } })); disposableStore.add(this._editorWidget.onDidContentSizeChange(() => this._layout())); @@ -648,21 +661,25 @@ export class AccessibleView extends Disposable { provider.id, provider.options, provider.provideContent.bind(provider), - provider.onClose, + provider.onClose.bind(provider), provider.verbositySettingKey, + provider.onOpen?.bind(provider), provider.actions, - provider.next, - provider.previous, - provider.onKeyDown, - provider.getSymbols, + provider.next?.bind(provider), + provider.previous?.bind(provider), + provider.onDidChangeContent?.bind(provider), + provider.onKeyDown?.bind(provider), + provider.getSymbols?.bind(provider), ) : new ExtensionContentProvider( provider.id, provider.options, provider.provideContent.bind(provider), - provider.onClose, - provider.next, - provider.previous, - provider.actions + provider.onClose.bind(provider), + provider.onOpen?.bind(provider), + provider.next?.bind(provider), + provider.previous?.bind(provider), + provider.actions, + provider.onDidChangeContent?.bind(provider), ); return lastProvider; } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index d4f6472c0c1..7aba4fa5baf 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -62,6 +62,7 @@ class AccessibleViewNextCodeBlockAction extends Action2 { mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, }, weight: KeybindingWeight.WorkbenchContrib, }, + // to icon: Codicon.arrowRight, menu: { diff --git a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts index 46ea96cf1f0..f8381417bd8 100644 --- a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts @@ -56,7 +56,8 @@ function registerAccessibilityHelpAction(keybindingService: IKeybindingService, () => content, () => viewsService.openView(viewDescriptor.id, true), ); - } + }, + dispose: () => { }, })); disposableStore.add(keybindingService.onDidUpdateKeybindings(() => { diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignal.contribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignal.contribution.ts index cb298c79733..deb77085819 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignal.contribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignal.contribution.ts @@ -10,11 +10,11 @@ import { registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/com import { AccessibilitySignalLineDebuggerContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution'; import { ShowAccessibilityAnnouncementHelp, ShowSignalSoundHelp } from 'vs/workbench/contrib/accessibilitySignals/browser/commands'; import { EditorTextPropertySignalsContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution'; -import { wrapInReloadableClass } from 'vs/workbench/contrib/accessibilitySignals/browser/reloadableWorkbenchContribution'; +import { wrapInReloadableClass0 } from 'vs/platform/observable/common/wrapInReloadableClass'; registerSingleton(IAccessibilitySignalService, AccessibilitySignalService, InstantiationType.Delayed); -registerWorkbenchContribution2('EditorTextPropertySignalsContribution', wrapInReloadableClass(() => EditorTextPropertySignalsContribution), WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2('EditorTextPropertySignalsContribution', wrapInReloadableClass0(() => EditorTextPropertySignalsContribution), WorkbenchPhase.AfterRestored); registerWorkbenchContribution2('AccessibilitySignalLineDebuggerContribution', AccessibilitySignalLineDebuggerContribution, WorkbenchPhase.AfterRestored); registerAction2(ShowSignalSoundHelp); diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts index 336832eb16c..f0bc218c0c1 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignalDebuggerContribution.ts @@ -19,7 +19,7 @@ export class AccessibilitySignalLineDebuggerContribution ) { super(); - const isEnabled = observableFromEvent( + const isEnabled = observableFromEvent(this, accessibilitySignalService.onSoundEnabledChanged(AccessibilitySignal.onDebugBreak), () => accessibilitySignalService.isSoundEnabled(AccessibilitySignal.onDebugBreak) ); diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts index de1c2f798af..f5dddf8ada4 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts @@ -35,7 +35,7 @@ export class EditorTextPropertySignalsContribution extends Disposable implements .some(signal => observableFromValueWithChangeEvent(this, this._accessibilitySignalService.getEnabledState(signal, false)).read(reader)) ); - private readonly _activeEditorObservable = observableFromEvent( + private readonly _activeEditorObservable = observableFromEvent(this, this._editorService.onDidActiveEditorChange, (_) => { const activeTextEditorControl = this._editorService.activeTextEditorControl; @@ -73,7 +73,7 @@ export class EditorTextPropertySignalsContribution extends Disposable implements let lastLine = -1; const ignoredLineSignalsForCurrentLine = new Set(); - const timeouts = new DisposableStore(); + const timeouts = store.add(new DisposableStore()); const propertySources = this._textProperties.map(p => ({ source: p.createSource(editor, editorModel), property: p })); @@ -104,7 +104,7 @@ export class EditorTextPropertySignalsContribution extends Disposable implements for (const modality of ['sound', 'announcement'] as AccessibilityModality[]) { if (this._accessibilitySignalService.getEnabledState(signal, false, modality).value) { - const delay = this._accessibilitySignalService.getDelayMs(signal, modality) + (didType.get() ? 1000 : 0); + const delay = this._accessibilitySignalService.getDelayMs(signal, modality, mode) + (didType.get() ? 1000 : 0); timeouts.add(disposableTimeout(() => { if (source.isPresent(position, mode, undefined)) { diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/reloadableWorkbenchContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/reloadableWorkbenchContribution.ts deleted file mode 100644 index 43fb32eed29..00000000000 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/reloadableWorkbenchContribution.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { isHotReloadEnabled } from 'vs/base/common/hotReload'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { autorunWithStore } from 'vs/base/common/observable'; -import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditor/utils'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; - -/** - * Wrap a class in a reloadable wrapper. - * When the wrapper is created, the original class is created. - * When the original class changes, the instance is re-created. -*/ -export function wrapInReloadableClass(getClass: () => (new (...args: any[]) => any)): (new (...args: any[]) => any) { - if (!isHotReloadEnabled()) { - return getClass(); - } - - return class ReloadableWrapper extends BaseClass { - private _autorun: IDisposable | undefined = undefined; - - override init() { - this._autorun = autorunWithStore((reader, store) => { - const clazz = readHotReloadableExport(getClass(), reader); - store.add(this.instantiationService.createInstance(clazz)); - }); - } - - dispose(): void { - this._autorun?.dispose(); - } - }; -} - -class BaseClass { - constructor( - @IInstantiationService protected readonly instantiationService: IInstantiationService, - ) { - this.init(); - } - - init(): void { } -} diff --git a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts index b5164a793db..8a4be09b66a 100644 --- a/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts +++ b/src/vs/workbench/contrib/accountEntitlements/browser/accountsEntitlements.contribution.ts @@ -151,7 +151,11 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut return [true, orgs && orgs.length > 0 ? (orgs[0].name ? orgs[0].name : orgs[0].login) : undefined]; } - private async enableEntitlements(session: AuthenticationSession) { + private async enableEntitlements(session: AuthenticationSession | undefined) { + if (!session) { + return; + } + const isInternal = isInternalTelemetry(this.productService, this.configurationService); const showAccountsBadge = this.configurationService.inspect(accountsBadgeConfigKey).value ?? false; const showWelcomeView = this.configurationService.inspect(chatWelcomeViewConfigKey).value ?? false; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 36ef150418a..a24720d5b75 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -3,44 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./bulkEdit'; -import { WorkbenchAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; -import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter, compareBulkFileOperations } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree'; -import { FuzzyScore } from 'vs/base/common/filters'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { localize } from 'vs/nls'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { BulkEditPreviewProvider, BulkFileOperation, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { URI } from 'vs/base/common/uri'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { MenuId } from 'vs/platform/actions/common/actions'; -import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import type { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { ButtonBar } from 'vs/base/browser/ui/button/button'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import type { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { CachedFunction, LRUCachedFunction } from 'vs/base/common/cache'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mutable } from 'vs/base/common/types'; -import { IResourceDiffEditorInput } from 'vs/workbench/common/editor'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./bulkEdit'; +import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { IMultiDiffEditorOptions, IMultiDiffResourceId } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl'; import { IRange } from 'vs/editor/common/core/range'; -import { CachedFunction, LRUCachedFunction } from 'vs/base/common/cache'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IOpenEvent, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ResourceLabels } from 'vs/workbench/browser/labels'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IMultiDiffEditorResource, IResourceDiffEditorInput } from 'vs/workbench/common/editor'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { BulkEditPreviewProvider, BulkFileOperation, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; +import { BulkEditAccessibilityProvider, BulkEditDataSource, BulkEditDelegate, BulkEditElement, BulkEditIdentityProvider, BulkEditNaviLabelProvider, BulkEditSorter, CategoryElement, CategoryElementRenderer, compareBulkFileOperations, FileElement, FileElementRenderer, TextEditElement, TextEditElementRenderer } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; const enum State { Data = 'data', @@ -56,7 +56,7 @@ export class BulkEditPane extends ViewPane { static readonly ctxGroupByFile = new RawContextKey('refactorPreview.groupByFile', true); static readonly ctxHasCheckedChanges = new RawContextKey('refactorPreview.hasCheckedChanges', true); - private static readonly _memGroupByFile = `${BulkEditPane.ID}.groupByFile`; + private static readonly _memGroupByFile = `${this.ID}.groupByFile`; private _tree!: WorkbenchAsyncDataTree; private _treeDataSource!: BulkEditDataSource; @@ -120,7 +120,7 @@ export class BulkEditPane extends ViewPane { const resourceLabels = this._instaService.createInstance( ResourceLabels, - { onDidChangeVisibility: this.onDidChangeBodyVisibility } + { onDidChangeVisibility: this.onDidChangeBodyVisibility } ); this._disposables.add(resourceLabels); @@ -369,16 +369,20 @@ export class BulkEditPane extends ViewPane { }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); } - private readonly _computeResourceDiffEditorInputs = new LRUCachedFunction(async (fileOperations: BulkFileOperation[]) => { - const computeDiffEditorInput = new CachedFunction>(async (fileOperation) => { + private readonly _computeResourceDiffEditorInputs = new LRUCachedFunction< + BulkFileOperation[], + Promise<{ resources: IMultiDiffEditorResource[]; getResourceDiffEditorInputIdOfOperation: (operation: BulkFileOperation) => Promise }> + >(async (fileOperations) => { + const computeDiffEditorInput = new CachedFunction>(async (fileOperation) => { const fileOperationUri = fileOperation.uri; const previewUri = this._currentProvider!.asPreviewUri(fileOperationUri); // delete if (fileOperation.type & BulkFileOperationType.Delete) { return { original: { resource: URI.revive(previewUri) }, - modified: { resource: undefined } - }; + modified: { resource: undefined }, + goToFileResource: fileOperation.uri, + } satisfies IMultiDiffEditorResource; } // rename, create, edits @@ -392,8 +396,9 @@ export class BulkEditPane extends ViewPane { } return { original: { resource: URI.revive(leftResource) }, - modified: { resource: URI.revive(previewUri) } - }; + modified: { resource: URI.revive(previewUri) }, + goToFileResource: leftResource, + } satisfies IMultiDiffEditorResource; } }); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts index 40fbb606f8b..44c687a6dee 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts @@ -353,7 +353,7 @@ export class BulkEditPreviewProvider implements ITextModelContentProvider { private static readonly Schema = 'vscode-bulkeditpreview-editor'; - static emptyPreview = URI.from({ scheme: BulkEditPreviewProvider.Schema, fragment: 'empty' }); + static emptyPreview = URI.from({ scheme: this.Schema, fragment: 'empty' }); static fromPreviewUri(uri: URI): URI { diff --git a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts index 22c507ca0b3..dc00b4d61d2 100644 --- a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts +++ b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { mockObject } from 'vs/base/test/common/mock'; diff --git a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts index a05e30502cc..50cd0b41f8c 100644 --- a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts +++ b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { IFileService } from 'vs/platform/files/common/files'; import { mock } from 'vs/workbench/test/common/workbenchTestServices'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index d7ec863b045..e7374ecb565 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -25,6 +25,7 @@ export class ChatAccessibilityHelp implements IAccessibleViewImplentation { const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'panelChat'); } + dispose() { } } export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat'): string { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index fa695e9d757..29731511a13 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -5,6 +5,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; @@ -13,18 +14,19 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IsLinuxContext, IsWindowsContext } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; +import { IQuickInputButton, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { clearChatEditor } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; import { CHAT_VIEW_ID, IChatWidgetService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatDetail, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export interface IChatViewTitleActionContext { @@ -101,10 +103,9 @@ class OpenChatGlobalAction extends Action2 { } } -class ChatHistoryAction extends ViewAction { +class ChatHistoryAction extends Action2 { constructor() { super({ - viewId: CHAT_VIEW_ID, id: `workbench.action.chat.history`, title: localize2('chat.history.label', "Show Chats..."), menu: { @@ -120,32 +121,63 @@ class ChatHistoryAction extends ViewAction { }); } - async runInView(accessor: ServicesAccessor, view: ChatViewPane) { + async run(accessor: ServicesAccessor) { const chatService = accessor.get(IChatService); const quickInputService = accessor.get(IQuickInputService); const viewsService = accessor.get(IViewsService); - const items = chatService.getHistory(); - const picks = items.map(i => ({ - label: i.title, - chat: i, - buttons: [{ - iconClass: ThemeIcon.asClassName(Codicon.x), - tooltip: localize('interactiveSession.history.delete', "Delete"), - }] - })); - const selection = await quickInputService.pick(picks, - { - placeHolder: localize('interactiveSession.history.pick', "Switch to chat"), - onDidTriggerItemButton: context => { - chatService.removeHistoryEntry(context.item.chat.sessionId); - context.removeItem(); - } - }); - if (selection) { - const sessionId = selection.chat.sessionId; - const view = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; - view.loadSession(sessionId); + const editorService = accessor.get(IEditorService); + + const openInEditorButton: IQuickInputButton = { + iconClass: ThemeIcon.asClassName(Codicon.file), + tooltip: localize('interactiveSession.history.editor', "Open in Editor"), + }; + const deleteButton: IQuickInputButton = { + iconClass: ThemeIcon.asClassName(Codicon.x), + tooltip: localize('interactiveSession.history.delete', "Delete"), + }; + + interface IChatPickerItem extends IQuickPickItem { + chat: IChatDetail; } + + const getPicks = () => { + const items = chatService.getHistory(); + return items.map((i): IChatPickerItem => ({ + label: i.title, + chat: i, + buttons: [ + openInEditorButton, + deleteButton + ] + })); + }; + + const store = new DisposableStore(); + const picker = store.add(quickInputService.createQuickPick()); + picker.placeholder = localize('interactiveSession.history.pick', "Switch to chat"); + picker.items = getPicks(); + store.add(picker.onDidTriggerItemButton(context => { + if (context.button === openInEditorButton) { + editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { sessionId: context.item.chat.sessionId }, pinned: true } }, ACTIVE_GROUP); + picker.hide(); + } else if (context.button === deleteButton) { + chatService.removeHistoryEntry(context.item.chat.sessionId); + picker.items = getPicks(); + } + })); + store.add(picker.onDidAccept(async () => { + try { + const item = picker.selectedItems[0]; + const sessionId = item.chat.sessionId; + const view = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; + view.loadSession(sessionId); + } finally { + picker.hide(); + } + })); + store.add(picker.onDidHide(() => store.dispose())); + + picker.show(); } } @@ -198,8 +230,26 @@ export function registerChatActions() { }); } async run(accessor: ServicesAccessor, ...args: any[]) { + const editorGroupsService = accessor.get(IEditorGroupsService); + const viewsService = accessor.get(IViewsService); + const chatService = accessor.get(IChatService); chatService.clearAllHistoryEntries(); + + const chatView = viewsService.getViewWithId(CHAT_VIEW_ID) as ChatViewPane | undefined; + if (chatView) { + chatView.widget.clear(); + } + + // Clear all chat editors. Have to go this route because the chat editor may be in the background and + // not have a ChatEditorInput. + editorGroupsService.groups.forEach(group => { + group.editors.forEach(editor => { + if (editor instanceof ChatEditorInput) { + clearChatEditor(accessor, editor); + } + }); + }); } }); @@ -260,6 +310,6 @@ export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewMod if (isRequestVM(item)) { return (includeName ? `${item.username}: ` : '') + item.messageText; } else { - return (includeName ? `${item.username}: ` : '') + item.response.asString(); + return (includeName ? `${item.username}: ` : '') + item.response.toString(); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts index 00ffcaed8c7..cd85340c42a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClear.ts @@ -6,18 +6,22 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -export async function clearChatEditor(accessor: ServicesAccessor): Promise { +export async function clearChatEditor(accessor: ServicesAccessor, chatEditorInput?: ChatEditorInput): Promise { const editorService = accessor.get(IEditorService); - const editorGroupsService = accessor.get(IEditorGroupsService); - const chatEditorInput = editorService.activeEditor; + if (!chatEditorInput) { + const editorInput = editorService.activeEditor; + chatEditorInput = editorInput instanceof ChatEditorInput ? editorInput : undefined; + } + if (chatEditorInput instanceof ChatEditorInput) { + // A chat editor can only be open in one group + const identifier = editorService.findEditors(chatEditorInput.resource)[0]; await editorService.replaceEditors([{ editor: chatEditorInput, replacement: { resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } satisfies IChatEditorOptions } - }], editorGroupsService.activeGroup); + }], identifier.groupId); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 8f2224e2fd4..1c6572f3415 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -16,7 +16,9 @@ import { CHAT_CATEGORY, isChatViewTitleActionContext } from 'vs/workbench/contri import { clearChatEditor } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; import { CHAT_VIEW_ID, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export const ACTION_ID_NEW_CHAT = `workbench.action.chat.newChat`; @@ -38,7 +40,7 @@ export function registerNewChatActions() { }); } async run(accessor: ServicesAccessor, ...args: any[]) { - announceChatCleared(accessor); + announceChatCleared(accessor.get(IAccessibilitySignalService)); await clearChatEditor(accessor); } }); @@ -73,22 +75,26 @@ export function registerNewChatActions() { }); } - run(accessor: ServicesAccessor, ...args: any[]) { + async run(accessor: ServicesAccessor, ...args: any[]) { const context = args[0]; + const accessibilitySignalService = accessor.get(IAccessibilitySignalService); if (isChatViewTitleActionContext(context)) { // Is running in the Chat view title - announceChatCleared(accessor); - context.chatView.clear(); + announceChatCleared(accessibilitySignalService); + context.chatView.widget.clear(); context.chatView.widget.focusInput(); } else { // Is running from f1 or keybinding const widgetService = accessor.get(IChatWidgetService); + const viewsService = accessor.get(IViewsService); - const widget = widgetService.lastFocusedWidget; + let widget = widgetService.lastFocusedWidget; if (!widget) { - return; + const chatView = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; + widget = chatView.widget; } - announceChatCleared(accessor); + + announceChatCleared(accessibilitySignalService); widget.clear(); widget.focusInput(); } @@ -96,6 +102,6 @@ export function registerNewChatActions() { }); } -function announceChatCleared(accessor: ServicesAccessor): void { - accessor.get(IAccessibilitySignalService).playSignal(AccessibilitySignal.clear); +function announceChatCleared(accessibilitySignalService: IAccessibilitySignalService): void { + accessibilitySignalService.playSignal(AccessibilitySignal.clear); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 455d4f15b17..378d6857e48 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -17,12 +17,14 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { CopyAction } from 'vs/editor/contrib/clipboard/browser/clipboard'; -import { localize2 } from 'vs/nls'; +import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { accessibleViewInCodeBlock } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; @@ -110,6 +112,7 @@ export function registerChatCodeBlockActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ agentId: context.element.agent?.id, + command: context.element.slashCommand?.name, sessionId: context.element.sessionId, requestId: context.element.requestId, result: context.element.result, @@ -155,6 +158,7 @@ export function registerChatCodeBlockActions() { if (element) { chatService.notifyUserAction({ agentId: element.agent?.id, + command: element.slashCommand?.name, sessionId: element.sessionId, requestId: element.requestId, result: element.result, @@ -182,7 +186,7 @@ export function registerChatCodeBlockActions() { constructor() { super({ id: 'workbench.action.chat.insertCodeBlock', - title: localize2('interactive.insertCodeBlock.label', "Insert at Cursor"), + title: localize2('interactive.insertCodeBlock.label', "Apply in Editor"), precondition: CONTEXT_CHAT_ENABLED, f1: true, category: CHAT_CATEGORY, @@ -267,6 +271,8 @@ export function registerChatCodeBlockActions() { const bulkEditService = accessor.get(IBulkEditService); const codeEditorService = accessor.get(ICodeEditorService); + const progressService = accessor.get(IProgressService); + const notificationService = accessor.get(INotificationService); const mappedEditsProviders = accessor.get(ILanguageFeaturesService).mappedEditsProvider.ordered(activeModel); @@ -275,7 +281,6 @@ export function registerChatCodeBlockActions() { let mappedEdits: WorkspaceEdit | null = null; if (mappedEditsProviders.length > 0) { - const mostRelevantProvider = mappedEditsProviders[0]; // TODO@ulugbekna: should we try all providers? // 0th sub-array - editor selections array if there are any selections // 1st sub-array - array with documents used to get the chat reply @@ -304,14 +309,37 @@ export function registerChatCodeBlockActions() { const cancellationTokenSource = new CancellationTokenSource(); - mappedEdits = await mostRelevantProvider.provideMappedEdits( - activeModel, - [codeBlockActionContext.code], - { documents: docRefs }, - cancellationTokenSource.token); + try { + mappedEdits = await progressService.withProgress( + { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, + async progress => { + progress.report({ message: localize('applyCodeBlock.progress', "Applying code block...") }); + + for (const provider of mappedEditsProviders) { + const mappedEdits = await provider.provideMappedEdits( + activeModel, + [codeBlockActionContext.code], + { documents: docRefs }, + cancellationTokenSource.token + ); + if (mappedEdits) { + return mappedEdits; + } + } + return null; + }, + () => cancellationTokenSource.cancel() + ); + } catch (e) { + notificationService.notify({ severity: Severity.Error, message: localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message) }); + } finally { + cancellationTokenSource.dispose(); + } + } if (mappedEdits) { + console.log('Mapped edits:', mappedEdits); await bulkEditService.apply(mappedEdits); } else { const activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); @@ -330,6 +358,7 @@ export function registerChatCodeBlockActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ agentId: context.element.agent?.id, + command: context.element.slashCommand?.name, sessionId: context.element.sessionId, requestId: context.element.requestId, result: context.element.result, @@ -375,6 +404,7 @@ export function registerChatCodeBlockActions() { if (isResponseVM(context.element)) { chatService.notifyUserAction({ agentId: context.element.agent?.id, + command: context.element.slashCommand?.name, sessionId: context.element.sessionId, requestId: context.element.requestId, result: context.element.result, @@ -467,6 +497,7 @@ export function registerChatCodeBlockActions() { if (isResponseVM(context.element)) { chatService.notifyUserAction({ agentId: context.element.agent?.id, + command: context.element.slashCommand?.name, sessionId: context.element.sessionId, requestId: context.element.requestId, result: context.element.result, @@ -497,7 +528,7 @@ export function registerChatCodeBlockActions() { const currentResponse = curCodeBlockInfo ? curCodeBlockInfo.element : (focusedResponse ?? widget.viewModel?.getItems().reverse().find((item): item is IChatResponseViewModel => isResponseVM(item))); - if (!currentResponse) { + if (!currentResponse || !isResponseVM(currentResponse)) { return; } @@ -610,7 +641,8 @@ export function registerChatCodeCompareBlockActions() { precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, CONTEXT_CHAT_EDIT_APPLIED.negate()), menu: { id: MenuId.ChatCompareBlock, - group: 'navigation' + group: 'navigation', + order: 1, } }); } @@ -621,7 +653,7 @@ export function registerChatCodeCompareBlockActions() { const instaService = accessor.get(IInstantiationService); const editor = instaService.createInstance(DefaultChatTextEditor); - await editor.apply(context.element, context.edit); + await editor.apply(context.element, context.edit, context.diffEditor); await editorService.openEditor({ resource: context.edit.uri, @@ -629,4 +661,28 @@ export function registerChatCodeCompareBlockActions() { }); } }); + + registerAction2(class DiscardEditsCompareBlockAction extends ChatCompareCodeBlockAction { + constructor() { + super({ + id: 'workbench.action.chat.discardCompareEdits', + title: localize2('interactive.compare.discard', "Discard Edits"), + f1: false, + category: CHAT_CATEGORY, + icon: Codicon.trash, + precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, CONTEXT_CHAT_EDIT_APPLIED.negate()), + menu: { + id: MenuId.ChatCompareBlock, + group: 'navigation', + order: 2, + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise { + const instaService = accessor.get(IInstantiationService); + const editor = instaService.createInstance(DefaultChatTextEditor); + editor.discard(context.element, context.edit); + } + }); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index d63dd9fe8b7..ee4b0beba48 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -7,10 +7,12 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Schemas } from 'vs/base/common/network'; +import { IRange } from 'vs/editor/common/core/range'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Command } from 'vs/editor/common/languages'; +import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess'; import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -20,21 +22,28 @@ import { IQuickInputService, IQuickPickItem, QuickPickItem } from 'vs/platform/q import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatContextAttachments } from 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; -import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_QUICK_CHAT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; +import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorType } from 'vs/editor/common/editorCommon'; +import { compare } from 'vs/base/common/strings'; export function registerChatContextActions() { registerAction2(AttachContextAction); + registerAction2(AttachFileAction); + registerAction2(AttachSelectionAction); } -export type IChatContextQuickPickItem = IFileQuickPickItem | IDynamicVariableQuickPickItem | IStaticVariableQuickPickItem; +export type IChatContextQuickPickItem = IFileQuickPickItem | IDynamicVariableQuickPickItem | IStaticVariableQuickPickItem | IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickAccessQuickPickItem; export interface IFileQuickPickItem extends IQuickPickItem { + kind: 'file'; id: string; name: string; value: URI; @@ -44,6 +53,7 @@ export interface IFileQuickPickItem extends IQuickPickItem { } export interface IDynamicVariableQuickPickItem extends IQuickPickItem { + kind: 'dynamic'; id: string; name?: string; value: unknown; @@ -54,6 +64,7 @@ export interface IDynamicVariableQuickPickItem extends IQuickPickItem { } export interface IStaticVariableQuickPickItem extends IQuickPickItem { + kind: 'static'; id: string; name: string; value: unknown; @@ -62,6 +73,67 @@ export interface IStaticVariableQuickPickItem extends IQuickPickItem { icon?: ThemeIcon; } +export interface IQuickAccessQuickPickItem extends IQuickPickItem { + kind: 'quickaccess'; + id: string; + name: string; + value: string; + + prefix: string; +} + +class AttachFileAction extends Action2 { + + static readonly ID = 'workbench.action.chat.attachFile'; + + constructor() { + super({ + id: AttachFileAction.ID, + title: localize2('workbench.action.chat.attachFile.label', "Attach File"), + category: CHAT_CATEGORY, + f1: false + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const variablesService = accessor.get(IChatVariablesService); + const textEditorService = accessor.get(IEditorService); + + const activeUri = textEditorService.activeEditor?.resource; + if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote].includes(activeUri.scheme)) { + variablesService.attachContext('file', activeUri, ChatAgentLocation.Panel); + } + } +} + +class AttachSelectionAction extends Action2 { + + static readonly ID = 'workbench.action.chat.attachSelection'; + + constructor() { + super({ + id: AttachSelectionAction.ID, + title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"), + category: CHAT_CATEGORY, + f1: false + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const variablesService = accessor.get(IChatVariablesService); + const textEditorService = accessor.get(IEditorService); + + const activeEditor = textEditorService.activeTextEditorControl; + const activeUri = textEditorService.activeEditor?.resource; + if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote].includes(activeUri.scheme)) { + const selection = activeEditor?.getSelection(); + if (selection) { + variablesService.attachContext('file', { uri: activeUri, range: selection }, ChatAgentLocation.Panel); + } + } + } +} + class AttachContextAction extends Action2 { static readonly ID = 'workbench.action.chat.attachContext'; @@ -79,12 +151,7 @@ class AttachContextAction extends Action2 { }, menu: [ { - when: CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), - id: MenuId.ChatExecuteSecondary, - group: 'group_1', - }, - { - when: CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), + when: ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), CONTEXT_IN_QUICK_CHAT.isEqualTo(false)), id: MenuId.ChatExecute, group: 'navigation', }, @@ -92,8 +159,14 @@ class AttachContextAction extends Action2 { }); } - private _getFileContextId(item: { resource: URI }) { - return item.resource.toString(); + private _getFileContextId(item: { resource: URI } | { uri: URI; range: IRange }) { + if ('resource' in item) { + return item.resource.toString(); + } + + return item.uri.toString() + (item.range.startLineNumber !== item.range.endLineNumber ? + `:${item.range.startLineNumber}-${item.range.endLineNumber}` : + `:${item.range.startLineNumber}`); } private async _attachContext(widget: IChatWidget, commandService: ICommandService, ...picks: IChatContextQuickPickItem[]) { @@ -114,21 +187,44 @@ class AttachContextAction extends Action2 { // Apply the original icon with the new name fullName: `${pick.icon ? `$(${pick.icon.id}) ` : ''}${selection}` }); - } else if (pick && typeof pick === 'object' && 'resource' in pick) { + } else if ('symbol' in pick && pick.symbol) { + // Symbol + toAttach.push({ + ...pick, + id: this._getFileContextId(pick.symbol.location), + value: pick.symbol.location, + fullName: pick.label, + name: pick.symbol.name, + isDynamic: true + }); + } else if (pick && typeof pick === 'object' && 'resource' in pick && pick.resource) { // #file variable toAttach.push({ ...pick, - id: this._getFileContextId(pick), + id: this._getFileContextId({ resource: pick.resource }), value: pick.resource, name: pick.label, + isFile: true, + isDynamic: true + }); + } else if ('symbolName' in pick && pick.uri && pick.range) { + // Symbol + toAttach.push({ + ...pick, + range: undefined, + id: this._getFileContextId({ uri: pick.uri, range: pick.range.decoration }), + value: { uri: pick.uri, range: pick.range.decoration }, + fullName: pick.label, + name: pick.symbolName!, isDynamic: true }); } else { // All other dynamic variables and static variables toAttach.push({ ...pick, - id: pick.id, - value: pick.value, + range: undefined, + id: pick.id ?? '', + value: 'value' in pick ? pick.value : undefined, fullName: pick.label, name: 'name' in pick && typeof pick.name === 'string' ? pick.name : pick.label, icon: 'icon' in pick && ThemeIcon.isThemeIcon(pick.icon) ? pick.icon : undefined @@ -186,29 +282,64 @@ class AttachContextAction extends Action2 { } - if (chatVariablesService.hasVariable(SelectAndInsertFileAction.Name)) { - quickPickItems.push(SelectAndInsertFileAction.Item, { type: 'separator' }); + quickPickItems.push({ + label: localize('chatContext.symbol', '{0} Symbol...', `$(${Codicon.symbolField.id})`), + icon: ThemeIcon.fromId(Codicon.symbolField.id), + prefix: SymbolsQuickAccessProvider.PREFIX + }); + + function extractTextFromIconLabel(label: string | undefined): string { + if (!label) { + return ''; + } + const match = label.match(/\$\([^\)]+\)\s*(.+)/); + return match ? match[1] : label; } - quickInputService.quickAccess.show('', { - enabledProviderPrefixes: [AnythingQuickAccessProvider.PREFIX], + this._show(quickInputService, commandService, widget, quickPickItems.sort(function (a, b) { + + const first = extractTextFromIconLabel(a.label).toUpperCase(); + const second = extractTextFromIconLabel(b.label).toUpperCase(); + + return compare(first, second); + })); + } + + private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[], query: string = '') { + quickInputService.quickAccess.show(query, { + enabledProviderPrefixes: [ + AnythingQuickAccessProvider.PREFIX, + SymbolsQuickAccessProvider.PREFIX, + AbstractGotoSymbolQuickAccessProvider.PREFIX + ], placeholder: localize('chatContext.attach.placeholder', 'Search attachments'), providerOptions: { handleAccept: (item: IChatContextQuickPickItem) => { - this._attachContext(widget, commandService, item); + if ('prefix' in item) { + this._show(quickInputService, commandService, widget, quickPickItems, item.prefix); + } else { + this._attachContext(widget, commandService, item); + } }, additionPicks: quickPickItems, - includeSymbols: false, filter: (item: IChatContextQuickPickItem) => { // Avoid attaching the same context twice const attachedContext = widget.getContrib(ChatContextAttachments.ID)?.getContext() ?? new Set(); + if ('symbol' in item && item.symbol) { + return !attachedContext.has(this._getFileContextId(item.symbol.location)); + } + if (item && typeof item === 'object' && 'resource' in item && URI.isUri(item.resource)) { return [Schemas.file, Schemas.vscodeRemote].includes(item.resource.scheme) && !attachedContext.has(this._getFileContextId({ resource: item.resource })); // Hack because Typescript doesn't narrow this type correctly } - if (!('command' in item)) { + if (item && typeof item === 'object' && 'uri' in item && item.uri && item.range) { + return !attachedContext.has(this._getFileContextId({ uri: item.uri, range: item.range.decoration })); + } + + if (!('command' in item) && item.id) { return !attachedContext.has(item.id); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts new file mode 100644 index 00000000000..4ab735e2b4e --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatDeveloperActions.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { localize2 } from 'vs/nls'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; + +export function registerChatDeveloperActions() { + registerAction2(LogChatInputHistoryAction); +} + +class LogChatInputHistoryAction extends Action2 { + + static readonly ID = 'workbench.action.chat.logInputHistory'; + + constructor() { + super({ + id: LogChatInputHistoryAction.ID, + title: localize2('workbench.action.chat.logInputHistory.label', "Log Chat Input History"), + icon: Codicon.attach, + category: Categories.Developer, + f1: true + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const chatWidgetService = accessor.get(IChatWidgetService); + chatWidgetService.lastFocusedWidget?.logInputHistory(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 5a6574db112..b7d76a5a227 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -161,6 +161,7 @@ export class CancelAction extends Action2 { keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.Escape, + win: { primary: KeyMod.Alt | KeyCode.Backspace }, } }); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts index 195ab2b3aa1..d04e49d6218 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts @@ -92,7 +92,6 @@ export function registerMoveActions() { async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNewLocation, chatView?: ChatViewPane) { const widgetService = accessor.get(IChatWidgetService); - const viewService = accessor.get(IViewsService); const editorService = accessor.get(IEditorService); const widget = chatView?.widget ?? widgetService.lastFocusedWidget; @@ -107,9 +106,8 @@ async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNew } const sessionId = viewModel.sessionId; - const view = await viewService.openView(widget.viewContext.viewId) as ChatViewPane; - const viewState = view.widget.getViewState(); - view.clear(); + const viewState = widget.getViewState(); + widget.clear(); await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { target: { sessionId }, pinned: true, viewState: viewState } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); } @@ -120,11 +118,14 @@ async function moveToSidebar(accessor: ServicesAccessor): Promise { const editorGroupService = accessor.get(IEditorGroupsService); const chatEditorInput = editorService.activeEditor; + let view: ChatViewPane; if (chatEditorInput instanceof ChatEditorInput && chatEditorInput.sessionId) { await editorService.closeEditor({ editor: chatEditorInput, groupId: editorGroupService.activeGroup.id }); - const view = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; + view = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; view.loadSession(chatEditorInput.sessionId); } else { - await viewsService.openView(CHAT_VIEW_ID); + view = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; } + + view.focus(); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 4a2455ea71f..9338bf5b51b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -51,6 +51,7 @@ export function registerChatTitleActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ agentId: item.agent?.id, + command: item.slashCommand?.name, sessionId: item.sessionId, requestId: item.requestId, result: item.result, @@ -90,6 +91,7 @@ export function registerChatTitleActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ agentId: item.agent?.id, + command: item.slashCommand?.name, sessionId: item.sessionId, requestId: item.requestId, result: item.result, @@ -128,6 +130,7 @@ export function registerChatTitleActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ agentId: item.agent?.id, + command: item.slashCommand?.name, sessionId: item.sessionId, requestId: item.requestId, result: item.result, @@ -174,7 +177,7 @@ export function registerChatTitleActions() { return; } - const value = item.response.asString(); + const value = item.response.toString(); const splitContents = splitMarkdownAndCodeBlocks(value); const focusRange = notebookEditor.getFocus(); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 7d45fc86091..9356ca139e5 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -43,10 +43,12 @@ import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/b import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; import 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputCompletions'; +import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover'; import { ChatAgentLocation, ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; +import { LanguageModelToolsService, ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; @@ -57,6 +59,8 @@ import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/s import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; import { registerChatContextActions } from 'vs/workbench/contrib/chat/browser/actions/chatContextActions'; +import { registerChatDeveloperActions } from 'vs/workbench/contrib/chat/browser/actions/chatDeveloperActions'; +import { LanguageModelToolsExtensionPointHandler } from 'vs/workbench/contrib/chat/common/tools/languageModelToolsContribution'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -97,10 +101,23 @@ configurationRegistry.registerConfiguration({ deprecated: true, default: false }, + 'chat.experimental.variables.editor': { + type: 'boolean', + description: nls.localize('chat.experimental.variables.editor', "Enables variables for editor chat."), + default: false + }, + 'chat.experimental.variables.notebook': { + type: 'boolean', + description: nls.localize('chat.experimental.variables.notebook', "Enables variables for notebook chat."), + default: false + }, + 'chat.experimental.variables.terminal': { + type: 'boolean', + description: nls.localize('chat.experimental.variables.terminal', "Enables variables for terminal chat."), + default: false + }, } }); - - Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( ChatEditor, @@ -159,7 +176,8 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { command: 'clear', detail: nls.localize('clear', "Start a new chat"), sortText: 'z2_clear', - executeImmediately: true + executeImmediately: true, + locations: [ChatAgentLocation.Panel] }, async () => { commandService.executeCommand(ACTION_ID_NEW_CHAT); })); @@ -167,7 +185,8 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { command: 'help', detail: '', sortText: 'z1_help', - executeImmediately: true + executeImmediately: true, + locations: [ChatAgentLocation.Panel] }, async (prompt, progress) => { const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); const agents = chatAgentService.getAgents(); @@ -236,6 +255,7 @@ registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribu workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlashCommandsContribution, LifecyclePhase.Eventually); Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); +registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore); registerChatActions(); registerChatCopyActions(); @@ -249,6 +269,7 @@ registerChatExportActions(); registerMoveActions(); registerNewChatActions(); registerChatContextActions(); +registerChatDeveloperActions(); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); @@ -261,5 +282,6 @@ registerSingleton(IChatSlashCommandService, ChatSlashCommandService, Instantiati registerSingleton(IChatAgentService, ChatAgentService, InstantiationType.Delayed); registerSingleton(IChatAgentNameService, ChatAgentNameService, InstantiationType.Delayed); registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed); +registerSingleton(ILanguageModelToolsService, LanguageModelToolsService, InstantiationType.Delayed); registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed); registerSingleton(IChatCodeBlockContextProviderService, ChatCodeBlockContextProviderService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 48844b393d8..99bb40c2ca4 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -13,7 +13,7 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { IChatViewState, IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableEntry, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; @@ -34,7 +34,6 @@ export interface IChatWidgetService { readonly lastFocusedWidget: IChatWidget | undefined; getWidgetByInputUri(uri: URI): IChatWidget | undefined; - getWidgetBySessionId(sessionId: string): IChatWidget | undefined; } @@ -79,7 +78,8 @@ export interface IChatAccessibilityService { export interface IChatCodeBlockInfo { codeBlockIndex: number; - element: IChatResponseViewModel; + element: ChatTreeItem; + uri: URI | undefined; focus(): void; } @@ -92,7 +92,7 @@ export interface IChatFileTreeInfo { export type ChatTreeItem = IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel; export interface IChatListItemRendererOptions { - readonly renderStyle?: 'default' | 'compact'; + readonly renderStyle?: 'default' | 'compact' | 'minimal'; readonly noHeader?: boolean; readonly noPadding?: boolean; readonly editableCodeBlock?: boolean; @@ -102,13 +102,22 @@ export interface IChatListItemRendererOptions { export interface IChatWidgetViewOptions { renderInputOnTop?: boolean; renderFollowups?: boolean; - renderStyle?: 'default' | 'compact'; + renderStyle?: 'default' | 'compact' | 'minimal'; supportsFileReferences?: boolean; filter?: (item: ChatTreeItem) => boolean; rendererOptions?: IChatListItemRendererOptions; menus?: { + /** + * The menu that is inside the input editor, use for send, dictation + */ executeToolbar?: MenuId; + /** + * The menu that next to the input editor, use for close, config etc + */ inputSideToolbar?: MenuId; + /** + * The telemetry source for all commands of this widget + */ telemetrySource?: string; }; defaultElementHeight?: number; @@ -148,6 +157,7 @@ export interface IChatWidget { getFocus(): ChatTreeItem | undefined; setInput(query?: string): void; getInput(): string; + logInputHistory(): void; acceptInput(query?: string): Promise; acceptInputWithPrefix(prefix: string): void; setInputPlaceholder(placeholder: string): void; @@ -161,10 +171,7 @@ export interface IChatWidget { getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined; setContext(overwrite: boolean, ...context: IChatRequestVariableEntry[]): void; clear(): void; -} - -export interface IChatViewPane { - clear(): void; + getViewState(): IChatViewState; } diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts index 5eb8edf7f6d..ecfaa1653f1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts @@ -62,16 +62,16 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider token.type === 'code')?.length ?? 0; + const codeBlockCount = marked.lexer(element.response.toString()).filter(token => token.type === 'code')?.length ?? 0; switch (codeBlockCount) { case 0: - label = accessibleViewHint ? localize('noCodeBlocksHint', "{0} {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('noCodeBlocks', "{0} {1}", fileTreeCountHint, element.response.asString()); + label = accessibleViewHint ? localize('noCodeBlocksHint', "{0} {1} {2}", fileTreeCountHint, element.response.toString(), accessibleViewHint) : localize('noCodeBlocks', "{0} {1}", fileTreeCountHint, element.response.toString()); break; case 1: - label = accessibleViewHint ? localize('singleCodeBlockHint', "{0} 1 code block: {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('singleCodeBlock', "{0} 1 code block: {1}", fileTreeCountHint, element.response.asString()); + label = accessibleViewHint ? localize('singleCodeBlockHint', "{0} 1 code block: {1} {2}", fileTreeCountHint, element.response.toString(), accessibleViewHint) : localize('singleCodeBlock', "{0} 1 code block: {1}", fileTreeCountHint, element.response.toString()); break; default: - label = accessibleViewHint ? localize('multiCodeBlockHint', "{0} {1} code blocks: {2}", fileTreeCountHint, codeBlockCount, element.response.asString(), accessibleViewHint) : localize('multiCodeBlock', "{0} {1} code blocks", fileTreeCountHint, codeBlockCount, element.response.asString()); + label = accessibleViewHint ? localize('multiCodeBlockHint', "{0} {1} code blocks: {2}", fileTreeCountHint, codeBlockCount, element.response.toString(), accessibleViewHint) : localize('multiCodeBlock', "{0} {1} code blocks", fileTreeCountHint, codeBlockCount, element.response.toString()); break; } return label; diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index 304fb89dbc9..06e0d0e292f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -11,6 +11,7 @@ import { AccessibilityProgressSignalScheduler } from 'vs/platform/accessibilityS import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; +import { MarkdownString } from 'vs/base/common/htmlContent'; const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; export class ChatAccessibilityService extends Disposable implements IChatAccessibilityService { @@ -33,13 +34,13 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number): void { this._pendingSignalMap.deleteAndDispose(requestId); const isPanelChat = typeof response !== 'string'; - const responseContent = typeof response === 'string' ? response : response?.response.asString(); + const responseContent = typeof response === 'string' ? response : response?.response.toString(); this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true }); if (!response || !responseContent) { return; } const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : ''; - status(renderStringAsPlaintext(responseContent) + errorDetails); + const plainTextResponse = renderStringAsPlaintext(new MarkdownString(responseContent)); + status(plainTextResponse + errorDetails); } } - diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 9163fd24fdb..78f34762cf0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { h } from 'vs/base/browser/dom'; -import { IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; +import { IManagedHoverOptions } from 'vs/base/browser/ui/hover/hover'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -29,6 +29,9 @@ export class ChatAgentHover extends Disposable { private readonly publisherName: HTMLElement; private readonly description: HTMLElement; + private readonly _onDidChangeContents = this._register(new Emitter()); + public readonly onDidChangeContents: Event = this._onDidChangeContents.event; + constructor( @IChatAgentService private readonly chatAgentService: IChatAgentService, @IExtensionsWorkbenchService private readonly extensionService: IExtensionsWorkbenchService, @@ -36,22 +39,22 @@ export class ChatAgentHover extends Disposable { ) { super(); - const hoverElement = h( + const hoverElement = dom.h( '.chat-agent-hover@root', [ - h('.chat-agent-hover-header', [ - h('.chat-agent-hover-icon@icon'), - h('.chat-agent-hover-details', [ - h('.chat-agent-hover-name@name'), - h('.chat-agent-hover-extension', [ - h('.chat-agent-hover-extension-name@extensionName'), - h('.chat-agent-hover-separator@separator'), - h('.chat-agent-hover-publisher@publisher'), + dom.h('.chat-agent-hover-header', [ + dom.h('.chat-agent-hover-icon@icon'), + dom.h('.chat-agent-hover-details', [ + dom.h('.chat-agent-hover-name@name'), + dom.h('.chat-agent-hover-extension', [ + dom.h('.chat-agent-hover-extension-name@extensionName'), + dom.h('.chat-agent-hover-separator@separator'), + dom.h('.chat-agent-hover-publisher@publisher'), ]), ]), ]), - h('.chat-agent-hover-warning@warning'), - h('span.chat-agent-hover-description@description'), + dom.h('.chat-agent-hover-warning@warning'), + dom.h('span.chat-agent-hover-description@description'), ]); this.domNode = hoverElement.root; @@ -110,13 +113,14 @@ export class ChatAgentHover extends Disposable { const extension = extensions[0]; if (extension?.publisherDomain?.verified) { this.domNode.classList.toggle('verifiedPublisher', true); + this._onDidChangeContents.fire(); } }); } } } -export function getChatAgentHoverOptions(getAgent: () => IChatAgentData | undefined, commandService: ICommandService): IUpdatableHoverOptions { +export function getChatAgentHoverOptions(getAgent: () => IChatAgentData | undefined, commandService: ICommandService): IManagedHoverOptions { return { actions: [ { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollections.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollections.ts new file mode 100644 index 00000000000..d13bbdff23b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollections.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; + +export class ResourcePool extends Disposable { + private readonly pool: T[] = []; + + private _inUse = new Set; + public get inUse(): ReadonlySet { + return this._inUse; + } + + constructor( + private readonly _itemFactory: () => T, + ) { + super(); + } + + get(): T { + if (this.pool.length > 0) { + const item = this.pool.pop()!; + this._inUse.add(item); + return item; + } + + const item = this._register(this._itemFactory()); + this._inUse.add(item); + return item; + } + + release(item: T): void { + this._inUse.delete(item); + this.pool.push(item); + } +} + +export interface IDisposableReference extends IDisposable { + object: T; + isStale: () => boolean; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart.ts new file mode 100644 index 00000000000..3893117fd20 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatCommandButton } from 'vs/workbench/contrib/chat/common/chatService'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +const $ = dom.$; + +export class ChatCommandButtonContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + constructor( + commandButton: IChatCommandButton, + context: IChatContentPartRenderContext, + @ICommandService private readonly commandService: ICommandService + ) { + super(); + + this.domNode = $('.chat-command-button'); + const enabled = !isResponseVM(context.element) || !context.element.isStale; + const tooltip = enabled ? + commandButton.command.tooltip : + localize('commandButtonDisabled', "Button not available in restored chat"); + const button = this._register(new Button(this.domNode, { ...defaultButtonStyles, supportIcons: true, title: tooltip })); + button.label = commandButton.command.title; + button.enabled = enabled; + + // TODO still need telemetry for command buttons + this._register(button.onDidClick(() => this.commandService.executeCommand(commandButton.command.id, ...(commandButton.command.arguments ?? [])))); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'command'; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts new file mode 100644 index 00000000000..4159f07c919 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ChatConfirmationWidget } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatConfirmation, IChatSendRequestOptions, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatConfirmationContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + constructor( + confirmation: IChatConfirmation, + context: IChatContentPartRenderContext, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatService private readonly chatService: IChatService, + ) { + super(); + + const element = context.element; + const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, [ + { label: localize('accept', "Accept"), data: confirmation.data }, + { label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true }, + ])); + confirmationWidget.setShowButtons(!confirmation.isUsed); + + this._register(confirmationWidget.onDidClick(async e => { + if (isResponseVM(element)) { + const prompt = `${e.label}: "${confirmation.title}"`; + const data: IChatSendRequestOptions = e.isSecondary ? + { rejectedConfirmationData: [e.data] } : + { acceptedConfirmationData: [e.data] }; + data.agentId = element.agent?.id; + data.slashCommand = element.slashCommand?.name; + if (await this.chatService.sendRequest(element.sessionId, prompt, data)) { + confirmation.isUsed = true; + confirmationWidget.setShowButtons(false); + this._onDidChangeHeight.fire(); + } + } + })); + + this.domNode = confirmationWidget.domNode; + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'confirmation'; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts similarity index 100% rename from src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts rename to src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts.ts new file mode 100644 index 00000000000..80e3c44a056 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatRendererContent } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export interface IChatContentPart extends IDisposable { + domNode: HTMLElement; + + /** + * Returns true if the other content is equivalent to what is already rendered in this content part. + * Returns false if a rerender is needed. + * followingContent is all the content that will be rendered after this content part (to support progress messages' behavior). + */ + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean; +} + +export interface IChatContentPartRenderContext { + element: ChatTreeItem; + index: number; + content: ReadonlyArray; + preceedingContentParts: ReadonlyArray; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts new file mode 100644 index 00000000000..29a2573374b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Emitter } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { Range } from 'vs/editor/common/core/range'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatCodeBlockInfo, IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; +import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; +import { CodeBlockPart, ICodeBlockData, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { IMarkdownVulnerability } from 'vs/workbench/contrib/chat/common/annotations'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; + +const $ = dom.$; + +export class ChatMarkdownContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + private readonly allRefs: IDisposableReference[] = []; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + public readonly codeblocks: IChatCodeBlockInfo[] = []; + + constructor( + private readonly markdown: IMarkdownString, + context: IChatContentPartRenderContext, + private readonly editorPool: EditorPool, + fillInIncompleteTokens = false, + codeBlockStartIndex = 0, + renderer: MarkdownRenderer, + currentWidth: number, + private readonly codeBlockModelCollection: CodeBlockModelCollection, + rendererOptions: IChatListItemRendererOptions, + @IContextKeyService contextKeyService: IContextKeyService, + @ITextModelService private readonly textModelService: ITextModelService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const element = context.element; + const markdownDecorationsRenderer = instantiationService.createInstance(ChatMarkdownDecorationsRenderer); + + // We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering + const orderedDisposablesList: IDisposable[] = []; + let codeBlockIndex = codeBlockStartIndex; + const result = this._register(renderer.render(markdown, { + fillInIncompleteTokens, + codeBlockRendererSync: (languageId, text) => { + const index = codeBlockIndex++; + let textModel: Promise; + let range: Range | undefined; + let vulns: readonly IMarkdownVulnerability[] | undefined; + if (equalsIgnoreCase(languageId, localFileLanguageId)) { + try { + const parsedBody = parseLocalFileData(text); + range = parsedBody.range && Range.lift(parsedBody.range); + textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object); + } catch (e) { + return $('div'); + } + } else { + if (!isRequestVM(element) && !isResponseVM(element)) { + console.error('Trying to render code block in welcome', element.id, index); + return $('div'); + } + + const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; + const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); + vulns = modelEntry.vulns; + textModel = modelEntry.model; + } + + const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; + const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns }, text, currentWidth, rendererOptions.editableCodeBlock); + this.allRefs.push(ref); + + // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) + // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) + this._register(ref.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); + + const info: IChatCodeBlockInfo = { + codeBlockIndex: index, + element, + focus() { + ref.object.focus(); + }, + uri: ref.object.uri + }; + this.codeblocks.push(info); + orderedDisposablesList.push(ref); + return ref.object.element; + }, + asyncRenderCallback: () => this._onDidChangeHeight.fire(), + })); + + this._register(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(result.element)); + + orderedDisposablesList.reverse().forEach(d => this._register(d)); + this.domNode = result.element; + } + + private renderCodeBlock(data: ICodeBlockData, text: string, currentWidth: number, editableCodeBlock: boolean | undefined): IDisposableReference { + const ref = this.editorPool.get(); + const editorInfo = ref.object; + if (isResponseVM(data.element)) { + this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }); + } + + editorInfo.render(data, currentWidth, editableCodeBlock); + + return ref; + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + return other.kind === 'markdownContent' && other.content.value === this.markdown.value; + } + + layout(width: number): void { + this.allRefs.forEach(ref => ref.object.layout(width)); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class EditorPool extends Disposable { + + private readonly _pool: ResourcePool; + + public inUse(): Iterable { + return this._pool.inUse; + } + + constructor( + options: ChatEditorOptions, + delegate: IChatRendererDelegate, + overflowWidgetsDomNode: HTMLElement | undefined, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => { + return instantiationService.createInstance(CodeBlockPart, options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode); + })); + } + + get(): IDisposableReference { + const codeBlock = this._pool.get(); + let stale = false; + return { + object: codeBlock, + isStale: () => stale, + dispose: () => { + codeBlock.reset(); + stale = true; + this._pool.release(codeBlock); + } + }; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts new file mode 100644 index 00000000000..6545fdfe9ad --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from 'vs/base/browser/dom'; +import { alert } from 'vs/base/browser/ui/aria/aria'; +import { Codicon } from 'vs/base/common/codicons'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressMessage, IChatTask } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatRendererContent, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatProgressContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly showSpinner: boolean; + + constructor( + progress: IChatProgressMessage | IChatTask, + renderer: MarkdownRenderer, + context: IChatContentPartRenderContext, + forceShowSpinner?: boolean, + forceShowMessage?: boolean + ) { + super(); + + const followingContent = context.content.slice(context.index + 1); + this.showSpinner = forceShowSpinner ?? shouldShowSpinner(followingContent, context.element); + const hideMessage = forceShowMessage !== true && followingContent.some(part => part.kind !== 'progressMessage'); + if (hideMessage) { + // Placeholder, don't show the progress message + this.domNode = $(''); + return; + } + + if (this.showSpinner) { + // TODO@roblourens is this the right place for this? + // this step is in progress, communicate it to SR users + alert(progress.content.value); + } + const codicon = this.showSpinner ? ThemeIcon.modify(Codicon.loading, 'spin').id : Codicon.check.id; + const markdown = new MarkdownString(`$(${codicon}) ${progress.content.value}`, { + supportThemeIcons: true + }); + const result = this._register(renderer.render(markdown)); + result.element.classList.add('progress-step'); + + this.domNode = result.element; + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + // Needs rerender when spinner state changes + const showSpinner = shouldShowSpinner(followingContent, element); + return other.kind === 'progressMessage' && this.showSpinner === showSpinner; + } +} + +function shouldShowSpinner(followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return isResponseVM(element) && !element.isComplete && followingContent.length === 0; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts new file mode 100644 index 00000000000..b1305e8e160 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -0,0 +1,299 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { matchesSomeScheme, Schemas } from 'vs/base/common/network'; +import { basename } from 'vs/base/common/path'; +import { basenameOrAuthority } from 'vs/base/common/resources'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { FileKind } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; +import { ColorScheme } from 'vs/workbench/browser/web.api'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatContentReference, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { IChatRendererContent, IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; + +const $ = dom.$; + +export class ChatReferencesContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + constructor( + private readonly data: ReadonlyArray, + labelOverride: string | undefined, + element: IChatResponseViewModel, + contentReferencesListPool: ContentReferencesListPool, + @IOpenerService openerService: IOpenerService, + ) { + super(); + + const referencesLabel = labelOverride ?? (data.length > 1 ? + localize('usedReferencesPlural', "Used {0} references", data.length) : + localize('usedReferencesSingular', "Used {0} reference", 1)); + const iconElement = $('.chat-used-context-icon'); + const icon = (element: IChatResponseViewModel) => element.usedReferencesExpanded ? Codicon.chevronDown : Codicon.chevronRight; + iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); + const buttonElement = $('.chat-used-context-label', undefined); + + const collapseButton = this._register(new Button(buttonElement, { + buttonBackground: undefined, + buttonBorder: undefined, + buttonForeground: undefined, + buttonHoverBackground: undefined, + buttonSecondaryBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryHoverBackground: undefined, + buttonSeparator: undefined + })); + this.domNode = $('.chat-used-context', undefined, buttonElement); + collapseButton.label = referencesLabel; + collapseButton.element.prepend(iconElement); + this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); + this.domNode.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); + this._register(collapseButton.onDidClick(() => { + iconElement.classList.remove(...ThemeIcon.asClassNameArray(icon(element))); + element.usedReferencesExpanded = !element.usedReferencesExpanded; + iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); + this.domNode.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); + this._onDidChangeHeight.fire(); + this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); + })); + + const ref = this._register(contentReferencesListPool.get()); + const list = ref.object; + this.domNode.appendChild(list.getHTMLElement().parentElement!); + + this._register(list.onDidOpen((e) => { + if (e.element && 'reference' in e.element) { + const uriOrLocation = 'variableName' in e.element.reference ? e.element.reference.value : e.element.reference; + const uri = URI.isUri(uriOrLocation) ? uriOrLocation : + uriOrLocation?.uri; + if (uri) { + openerService.open( + uri, + { + fromUserGesture: true, + editorOptions: { + ...e.editorOptions, + ...{ + selection: uriOrLocation && 'range' in uriOrLocation ? uriOrLocation.range : undefined + } + } + }); + } + } + })); + this._register(list.onContextMenu((e) => { + e.browserEvent.preventDefault(); + e.browserEvent.stopPropagation(); + })); + + const maxItemsShown = 6; + const itemsShown = Math.min(data.length, maxItemsShown); + const height = itemsShown * 22; + list.layout(height); + list.getHTMLElement().style.height = `${height}px`; + list.splice(0, list.length, data); + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return other.kind === 'references' && other.references.length === this.data.length; + } + + private updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { + element.ariaLabel = expanded ? localize('usedReferencesExpanded', "{0}, expanded", label) : localize('usedReferencesCollapsed', "{0}, collapsed", label); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class ContentReferencesListPool extends Disposable { + private _pool: ResourcePool>; + + public get inUse(): ReadonlySet> { + return this._pool.inUse; + } + + constructor( + private _onDidChangeVisibility: Event, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IThemeService private readonly themeService: IThemeService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => this.listFactory())); + } + + private listFactory(): WorkbenchList { + const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); + + const container = $('.chat-used-context-list'); + this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); + + const list = this.instantiationService.createInstance( + WorkbenchList, + 'ChatListRenderer', + container, + new ContentReferencesListDelegate(), + [this.instantiationService.createInstance(ContentReferencesListRenderer, resourceLabels)], + { + alwaysConsumeMouseWheel: false, + accessibilityProvider: { + getAriaLabel: (element: IChatContentReference | IChatWarningMessage) => { + if (element.kind === 'warning') { + return element.content.value; + } + const reference = element.reference; + if ('variableName' in reference) { + return reference.variableName; + } else if (URI.isUri(reference)) { + return basename(reference.path); + } else { + return basename(reference.uri.path); + } + }, + + getWidgetAriaLabel: () => localize('usedReferences', "Used References") + }, + dnd: { + getDragURI: (element: IChatContentReference | IChatWarningMessage) => { + if (element.kind === 'warning') { + return null; + } + const { reference } = element; + if ('variableName' in reference) { + return null; + } else if (URI.isUri(reference)) { + return reference.toString(); + } else { + return reference.uri.toString(); + } + }, + dispose: () => { }, + onDragOver: () => false, + drop: () => { }, + }, + }); + + return list; + } + + get(): IDisposableReference> { + const object = this._pool.get(); + let stale = false; + return { + object, + isStale: () => stale, + dispose: () => { + stale = true; + this._pool.release(object); + } + }; + } +} + +class ContentReferencesListDelegate implements IListVirtualDelegate { + getHeight(element: IChatContentReference): number { + return 22; + } + + getTemplateId(element: IChatContentReference): string { + return ContentReferencesListRenderer.TEMPLATE_ID; + } +} + +interface IChatContentReferenceListTemplate { + label: IResourceLabel; + templateDisposables: IDisposable; +} + +class ContentReferencesListRenderer implements IListRenderer { + static TEMPLATE_ID = 'contentReferencesListRenderer'; + readonly templateId: string = ContentReferencesListRenderer.TEMPLATE_ID; + + constructor( + private labels: ResourceLabels, + @IThemeService private readonly themeService: IThemeService, + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + ) { } + + renderTemplate(container: HTMLElement): IChatContentReferenceListTemplate { + const templateDisposables = new DisposableStore(); + const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); + return { templateDisposables, label }; + } + + + private getReferenceIcon(data: IChatContentReference): URI | ThemeIcon | undefined { + if (ThemeIcon.isThemeIcon(data.iconPath)) { + return data.iconPath; + } else { + return this.themeService.getColorTheme().type === ColorScheme.DARK && data.iconPath?.dark + ? data.iconPath?.dark + : data.iconPath?.light; + } + } + + renderElement(data: IChatContentReference | IChatWarningMessage, index: number, templateData: IChatContentReferenceListTemplate, height: number | undefined): void { + if (data.kind === 'warning') { + templateData.label.setResource({ name: data.content.value }, { icon: Codicon.warning }); + return; + } + + const reference = data.reference; + const icon = this.getReferenceIcon(data); + templateData.label.element.style.display = 'flex'; + if ('variableName' in reference) { + if (reference.value) { + const uri = URI.isUri(reference.value) ? reference.value : reference.value.uri; + templateData.label.setResource( + { + resource: uri, + name: basenameOrAuthority(uri), + description: `#${reference.variableName}`, + range: 'range' in reference.value ? reference.value.range : undefined, + }, { icon }); + } else { + const variable = this.chatVariablesService.getVariable(reference.variableName); + templateData.label.setLabel(`#${reference.variableName}`, undefined, { title: variable?.description }); + } + } else { + const uri = 'uri' in reference ? reference.uri : reference; + if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) { + templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: icon ?? Codicon.globe }); + } else { + templateData.label.setFile(uri, { + fileKind: FileKind.FILE, + // Should not have this live-updating data on a historical reference + fileDecorations: { badges: false, colors: false }, + range: 'range' in reference ? reference.range : undefined + }); + } + } + } + + disposeTemplate(templateData: IChatContentReferenceListTemplate): void { + templateData.templateDisposables.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts new file mode 100644 index 00000000000..a1a03e1cae1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { ChatProgressContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart'; +import { ChatReferencesContentPart, ContentReferencesListPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatTask } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatTaskContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + public readonly onDidChangeHeight: Event; + + constructor( + private readonly task: IChatTask, + contentReferencesListPool: ContentReferencesListPool, + renderer: MarkdownRenderer, + context: IChatContentPartRenderContext, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + if (task.progress.length) { + const refsPart = this._register(instantiationService.createInstance(ChatReferencesContentPart, task.progress, task.content.value, context.element as IChatResponseViewModel, contentReferencesListPool)); + this.domNode = dom.$('.chat-progress-task'); + this.domNode.appendChild(refsPart.domNode); + this.onDidChangeHeight = refsPart.onDidChangeHeight; + } else { + // #217645 + const isSettled = task.isSettled?.() ?? true; + const progressPart = this._register(instantiationService.createInstance(ChatProgressContentPart, task, renderer, context, !isSettled, true)); + this.domNode = progressPart.domNode; + this.onDidChangeHeight = Event.None; + } + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + return other.kind === 'progressTask' + && other.progress.length === this.task.progress.length + && other.isSettled() === this.task.isSettled(); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.ts new file mode 100644 index 00000000000..5f4e6556c5c --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.ts @@ -0,0 +1,211 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { TextEdit } from 'vs/editor/common/languages'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; +import { IModelService } from 'vs/editor/common/services/model'; +import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; +import { CodeCompareBlockPart, ICodeCompareBlockData, ICodeCompareBlockDiffData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +const $ = dom.$; + +export class ChatTextEditContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + private readonly ref: IDisposableReference | undefined; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + constructor( + chatTextEdit: IChatTextEditGroup, + context: IChatContentPartRenderContext, + rendererOptions: IChatListItemRendererOptions, + diffEditorPool: DiffEditorPool, + currentWidth: number, + @ITextModelService private readonly textModelService: ITextModelService, + @IModelService private readonly modelService: IModelService, + @IChatService private readonly chatService: IChatService, + ) { + super(); + const element = context.element; + + // TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen + if (rendererOptions.renderTextEditsAsSummary?.(chatTextEdit.uri)) { + if (isResponseVM(element) && element.response.value.every(item => item.kind === 'textEditGroup')) { + this.domNode = $('.interactive-edits-summary', undefined, !element.isComplete + ? localize('editsSummary1', "Making changes...") + : element.isCanceled + ? localize('edits0', "Making changes was aborted.") + : localize('editsSummary', "Made changes.")); + } else { + this.domNode = $('div'); + } + + // TODO@roblourens this case is now handled outside this Part in ChatListRenderer, but can it be cleaned up? + // return; + } else { + + + const cts = new CancellationTokenSource(); + + let isDisposed = false; + this._register(toDisposable(() => { + isDisposed = true; + cts.dispose(true); + })); + + this.ref = this._register(diffEditorPool.get()); + + // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) + // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) + this._register(this.ref.object.onDidChangeContentHeight(() => { + this._onDidChangeHeight.fire(); + })); + + const data: ICodeCompareBlockData = { + element, + edit: chatTextEdit, + diffData: (async () => { + + const ref = await this.textModelService.createModelReference(chatTextEdit.uri); + + if (isDisposed) { + ref.dispose(); + return; + } + + this._register(ref); + + const original = ref.object.textEditorModel; + let originalSha1: string = ''; + + if (chatTextEdit.state) { + originalSha1 = chatTextEdit.state.sha1; + } else { + const sha1 = new DefaultModelSHA1Computer(); + if (sha1.canComputeSHA1(original)) { + originalSha1 = sha1.computeSHA1(original); + chatTextEdit.state = { sha1: originalSha1, applied: 0 }; + } + } + + const modified = this.modelService.createModel( + createTextBufferFactoryFromSnapshot(original.createSnapshot()), + { languageId: original.getLanguageId(), onDidChange: Event.None }, + URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: original.uri.path, query: generateUuid() }), + false + ); + const modRef = await this.textModelService.createModelReference(modified.uri); + this._register(modRef); + + const editGroups: ISingleEditOperation[][] = []; + if (isResponseVM(element)) { + const chatModel = this.chatService.getSession(element.sessionId)!; + + for (const request of chatModel.getRequests()) { + if (!request.response) { + continue; + } + for (const item of request.response.response.value) { + if (item.kind !== 'textEditGroup' || item.state?.applied || !isEqual(item.uri, chatTextEdit.uri)) { + continue; + } + for (const group of item.edits) { + const edits = group.map(TextEdit.asEditOperation); + editGroups.push(edits); + } + } + if (request.response === element.model) { + break; + } + } + } + + for (const edits of editGroups) { + modified.pushEditOperations(null, edits, () => null); + } + + return { + modified, + original, + originalSha1 + } satisfies ICodeCompareBlockDiffData; + })() + }; + this.ref.object.render(data, currentWidth, cts.token); + + this.domNode = this.ref.object.element; + } + } + + layout(width: number): void { + this.ref?.object.layout(width); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'textEditGroup'; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class DiffEditorPool extends Disposable { + + private readonly _pool: ResourcePool; + + public inUse(): Iterable { + return this._pool.inUse; + } + + constructor( + options: ChatEditorOptions, + delegate: IChatRendererDelegate, + overflowWidgetsDomNode: HTMLElement | undefined, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => { + return instantiationService.createInstance(CodeCompareBlockPart, options, MenuId.ChatCompareBlock, delegate, overflowWidgetsDomNode); + })); + } + + get(): IDisposableReference { + const codeBlock = this._pool.get(); + let stale = false; + return { + object: codeBlock, + isStale: () => stale, + dispose: () => { + codeBlock.reset(); + stale = true; + this._pool.release(codeBlock); + } + }; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart.ts new file mode 100644 index 00000000000..8742d2da602 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart.ts @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { IAsyncDataSource, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { FileKind, FileType } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; +import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; +import { IChatContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService'; +import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; +import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; + +const $ = dom.$; + +export class ChatTreeContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + public readonly onDidFocus: Event; + + private tree: WorkbenchCompressibleAsyncDataTree; + + constructor( + data: IChatResponseProgressFileTreeData, + element: ChatTreeItem, + treePool: TreePool, + treeDataIndex: number, + @IOpenerService private readonly openerService: IOpenerService + ) { + super(); + + const ref = this._register(treePool.get()); + this.tree = ref.object; + this.onDidFocus = this.tree.onDidFocus; + + this._register(this.tree.onDidOpen((e) => { + if (e.element && !('children' in e.element)) { + this.openerService.open(e.element.uri); + } + })); + this._register(this.tree.onDidChangeCollapseState(() => { + this._onDidChangeHeight.fire(); + })); + this._register(this.tree.onContextMenu((e) => { + e.browserEvent.preventDefault(); + e.browserEvent.stopPropagation(); + })); + + this.tree.setInput(data).then(() => { + if (!ref.isStale()) { + this.tree.layout(); + this._onDidChangeHeight.fire(); + } + }); + + this.domNode = this.tree.getHTMLElement().parentElement!; + } + + domFocus() { + this.tree.domFocus(); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'treeData'; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} + +export class TreePool extends Disposable { + private _pool: ResourcePool>; + + public get inUse(): ReadonlySet> { + return this._pool.inUse; + } + + constructor( + private _onDidChangeVisibility: Event, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConfigurationService private readonly configService: IConfigurationService, + @IThemeService private readonly themeService: IThemeService, + ) { + super(); + this._pool = this._register(new ResourcePool(() => this.treeFactory())); + } + + private treeFactory(): WorkbenchCompressibleAsyncDataTree { + const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); + + const container = $('.interactive-response-progress-tree'); + this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); + + const tree = this.instantiationService.createInstance( + WorkbenchCompressibleAsyncDataTree, + 'ChatListRenderer', + container, + new ChatListTreeDelegate(), + new ChatListTreeCompressionDelegate(), + [new ChatListTreeRenderer(resourceLabels, this.configService.getValue('explorer.decorations'))], + new ChatListTreeDataSource(), + { + collapseByDefault: () => false, + expandOnlyOnTwistieClick: () => false, + identityProvider: { + getId: (e: IChatResponseProgressFileTreeData) => e.uri.toString() + }, + accessibilityProvider: { + getAriaLabel: (element: IChatResponseProgressFileTreeData) => element.label, + getWidgetAriaLabel: () => localize('treeAriaLabel', "File Tree") + }, + alwaysConsumeMouseWheel: false + }); + + return tree; + } + + get(): IDisposableReference> { + const object = this._pool.get(); + let stale = false; + return { + object, + isStale: () => stale, + dispose: () => { + stale = true; + this._pool.release(object); + } + }; + } +} + +class ChatListTreeDelegate implements IListVirtualDelegate { + static readonly ITEM_HEIGHT = 22; + + getHeight(element: IChatResponseProgressFileTreeData): number { + return ChatListTreeDelegate.ITEM_HEIGHT; + } + + getTemplateId(element: IChatResponseProgressFileTreeData): string { + return 'chatListTreeTemplate'; + } +} + +class ChatListTreeCompressionDelegate implements ITreeCompressionDelegate { + isIncompressible(element: IChatResponseProgressFileTreeData): boolean { + return !element.children; + } +} + +interface IChatListTreeRendererTemplate { + templateDisposables: DisposableStore; + label: IResourceLabel; +} + +class ChatListTreeRenderer implements ICompressibleTreeRenderer { + templateId: string = 'chatListTreeTemplate'; + + constructor(private labels: ResourceLabels, private decorations: IFilesConfiguration['explorer']['decorations']) { } + + renderCompressedElements(element: ITreeNode, void>, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { + templateData.label.element.style.display = 'flex'; + const label = element.element.elements.map((e) => e.label); + templateData.label.setResource({ resource: element.element.elements[0].uri, name: label }, { + title: element.element.elements[0].label, + fileKind: element.children ? FileKind.FOLDER : FileKind.FILE, + extraClasses: ['explorer-item'], + fileDecorations: this.decorations + }); + } + renderTemplate(container: HTMLElement): IChatListTreeRendererTemplate { + const templateDisposables = new DisposableStore(); + const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); + return { templateDisposables, label }; + } + renderElement(element: ITreeNode, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { + templateData.label.element.style.display = 'flex'; + if (!element.children.length && element.element.type !== FileType.Directory) { + templateData.label.setFile(element.element.uri, { + fileKind: FileKind.FILE, + hidePath: true, + fileDecorations: this.decorations, + }); + } else { + templateData.label.setResource({ resource: element.element.uri, name: element.element.label }, { + title: element.element.label, + fileKind: FileKind.FOLDER, + fileDecorations: this.decorations + }); + } + } + disposeTemplate(templateData: IChatListTreeRendererTemplate): void { + templateData.templateDisposables.dispose(); + } +} + +class ChatListTreeDataSource implements IAsyncDataSource { + hasChildren(element: IChatResponseProgressFileTreeData): boolean { + return !!element.children; + } + + async getChildren(element: IChatResponseProgressFileTreeData): Promise> { + return element.children ?? []; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart.ts new file mode 100644 index 00000000000..3fd0b9fb239 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Codicon } from 'vs/base/common/codicons'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { IChatContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; + +const $ = dom.$; + +export class ChatWarningContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + constructor( + kind: 'info' | 'warning' | 'error', + content: IMarkdownString, + renderer: MarkdownRenderer, + ) { + super(); + + this.domNode = $('.chat-notification-widget'); + let icon; + let iconClass; + switch (kind) { + case 'warning': + icon = Codicon.warning; + iconClass = '.chat-warning-codicon'; + break; + case 'error': + icon = Codicon.error; + iconClass = '.chat-error-codicon'; + break; + case 'info': + icon = Codicon.info; + iconClass = '.chat-info-codicon'; + break; + } + this.domNode.appendChild($(iconClass, undefined, renderIcon(icon))); + const markdownContent = renderer.render(content); + this.domNode.appendChild(markdownContent.element); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'warning'; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css similarity index 100% rename from src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css rename to src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 1da5d57b810..94fe53297a8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -50,13 +50,15 @@ export class ChatEditor extends EditorPane { super(ChatEditorInput.EditorID, group, telemetryService, themeService, storageService); } - public async clear() { - return this.instantiationService.invokeFunction(clearChatEditor); + private async clear() { + if (this.input) { + return this.instantiationService.invokeFunction(clearChatEditor, this.input as ChatEditorInput); + } } protected override createEditor(parent: HTMLElement): void { this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent)); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); this.widget = this._register( scopedInstantiationService.createInstance( @@ -75,7 +77,7 @@ export class ChatEditor extends EditorPane { this.widget.setVisible(true); } - protected override setEditorVisible(visible: boolean): void { + protected override setEditorVisible(visible: boolean): void { super.setEditorVisible(visible); this.widget?.setVisible(visible); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index dc56a5c97aa..9fcc9fb8e50 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -6,14 +6,16 @@ import * as dom from 'vs/base/browser/dom'; import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { Range } from 'vs/editor/common/core/range'; import { Button } from 'vs/base/browser/ui/button/button'; import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; -import { HistoryNavigator } from 'vs/base/common/history'; +import { HistoryNavigator2 } from 'vs/base/common/history'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { basename, dirname } from 'vs/base/common/path'; import { isMacintosh } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; @@ -21,6 +23,7 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IDimension } from 'vs/editor/common/core/dimension'; import { IPosition } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; @@ -38,6 +41,7 @@ import { registerAndCreateHistoryNavigationContext } from 'vs/platform/history/b import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ResourceLabels } from 'vs/workbench/browser/labels'; @@ -95,6 +99,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._attachedContext; } + private _indexOfLastAttachedContextDeletedWithKeyboard: number = -1; private readonly _attachedContext = new Set(); private readonly _onDidChangeVisibility = this._register(new Emitter()); @@ -126,10 +131,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._inputEditor; } - private history: HistoryNavigator; + private history: HistoryNavigator2; private historyNavigationBackwardsEnablement!: IContextKey; private historyNavigationForewardsEnablement!: IContextKey; - private onHistoryEntry = false; private inHistoryNavigation = false; private inputModel: ITextModel | undefined; private inputEditorHasText: IContextKey; @@ -152,6 +156,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IConfigurationService private readonly configurationService: IConfigurationService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @ILogService private readonly logService: ILogService, ) { super(); @@ -161,8 +166,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatCursorAtTop = CONTEXT_CHAT_INPUT_CURSOR_AT_TOP.bindTo(contextKeyService); this.inputEditorHasFocus = CONTEXT_CHAT_INPUT_HAS_FOCUS.bindTo(contextKeyService); - this.history = new HistoryNavigator([], 5); - this._register(this.historyService.onDidClearHistory(() => this.history.clear())); + this.history = this.loadHistory(); + this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], 50, historyKeyFn))); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { @@ -171,6 +176,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); } + private loadHistory(): HistoryNavigator2 { + const history = this.historyService.getHistory(this.location); + if (history.length === 0) { + history.push({ text: '' }); + } + + return new HistoryNavigator2(history, 50, historyKeyFn); + } + private _getAriaLabel(): string { const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat); if (verbose) { @@ -180,13 +194,35 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return localize('chatInput', "Chat Input"); } - setState(inputValue: string | undefined): void { - const history = this.historyService.getHistory(this.location); - this.history = new HistoryNavigator(history, 50); - - if (typeof inputValue === 'string') { - this.setValue(inputValue); + updateState(inputState: Object): void { + if (this.inHistoryNavigation) { + return; } + + const newEntry = { text: this._inputEditor.getValue(), state: inputState }; + + if (this.history.isAtEnd()) { + // The last history entry should always be the current input value + this.history.replaceLast(newEntry); + } else { + // Added a reference while in the middle of history navigation, it's a new entry + this.history.replaceLast(newEntry); + this.history.resetCursor(); + } + } + + initForNewChatModel(inputValue: string | undefined, inputState: Object): void { + this.history = this.loadHistory(); + this.history.add({ text: inputValue ?? this.history.current().text, state: inputState }); + + if (inputValue) { + this.setValue(inputValue, false); + } + } + + logInputHistory(): void { + const historyStr = [...this.history].map(entry => JSON.stringify(entry)).join('\n'); + this.logService.info(`[${this.location}] Chat input history:`, historyStr); } setVisible(visible: boolean): void { @@ -198,24 +234,39 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } showPreviousValue(): void { + if (this.history.isAtEnd()) { + this.saveCurrentValue(); + } else { + if (!this.history.has({ text: this._inputEditor.getValue(), state: this.history.current().state })) { + this.saveCurrentValue(); + this.history.resetCursor(); + } + } + this.navigateHistory(true); } showNextValue(): void { + if (this.history.isAtEnd()) { + return; + } else { + if (!this.history.has({ text: this._inputEditor.getValue(), state: this.history.current().state })) { + this.saveCurrentValue(); + this.history.resetCursor(); + } + } + this.navigateHistory(false); } private navigateHistory(previous: boolean): void { - const historyEntry = (previous ? - (this.history.previous() ?? this.history.first()) : this.history.next()) - ?? { text: '' }; - - this.onHistoryEntry = previous || this.history.current() !== null; + const historyEntry = previous ? + this.history.previous() : this.history.next(); aria.status(historyEntry.text); this.inHistoryNavigation = true; - this.setValue(historyEntry.text); + this.setValue(historyEntry.text, true); this.inHistoryNavigation = false; this._onDidLoadInputState.fire(historyEntry.state); @@ -231,10 +282,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - setValue(value: string): void { + setValue(value: string, transient: boolean): void { this.inputEditor.setValue(value); // always leave cursor at the end this.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 }); + + if (!transient) { + this.saveCurrentValue(); + } + } + + private saveCurrentValue(): void { + const newEntry = { text: this._inputEditor.getValue(), state: this.history.current().state }; + this.history.replaceLast(newEntry); } focus() { @@ -249,17 +309,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge * Reset the input and update history. * @param userQuery If provided, this will be added to the history. Followups and programmatic queries should not be passed. */ - async acceptInput(userQuery?: string, inputState?: any): Promise { - if (userQuery) { - let element = this.history.getHistory().find(candidate => candidate.text === userQuery); - if (!element) { - element = { text: userQuery, state: inputState }; - } else { - element.state = inputState; - } - this.history.add(element); + async acceptInput(isUserQuery?: boolean): Promise { + if (isUserQuery) { + const userQuery = this._inputEditor.getValue(); + const entry: IChatHistoryEntry = { text: userQuery, state: this.history.current().state }; + this.history.replaceLast(entry); + this.history.add({ text: '' }); } + this._onDidLoadInputState.fire({}); if (this.accessibilityService.isScreenReaderOptimized() && isMacintosh) { this._acceptInputForVoiceover(); } else { @@ -275,7 +333,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } // Remove the input editor from the DOM temporarily to prevent VoiceOver // from reading the cleared text (the request) to the user. - this._inputEditorElement.removeChild(domNode); + domNode.remove(); this._inputEditor.setValue(''); this._inputEditorElement.appendChild(domNode); this._inputEditor.focus(); @@ -301,7 +359,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(inputContainer)); CONTEXT_IN_CHAT_INPUT.bindTo(inputScopedContextKeyService).set(true); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService])); + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService]))); const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(inputScopedContextKeyService, this)); this.historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement; @@ -339,21 +397,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._onDidChangeHeight.fire(); } - // Only allow history navigation when the input is empty. - // (If this model change happened as a result of a history navigation, this is canceled out by a call in this.navigateHistory) const model = this._inputEditor.getModel(); const inputHasText = !!model && model.getValue().trim().length > 0; this.inputEditorHasText.set(inputHasText); - - // If the user is typing on a history entry, then reset the onHistoryEntry flag so that history navigation can be disabled - if (!this.inHistoryNavigation) { - this.onHistoryEntry = false; - } - - if (!this.onHistoryEntry) { - this.historyNavigationForewardsEnablement.set(!inputHasText); - this.historyNavigationBackwardsEnablement.set(!inputHasText); - } })); this._register(this._inputEditor.onDidFocusEditorText(() => { this.inputEditorHasFocus.set(true); @@ -375,10 +421,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const atTop = e.position.column === 1 && e.position.lineNumber === 1; this.chatCursorAtTop.set(atTop); - if (this.onHistoryEntry) { - this.historyNavigationBackwardsEnablement.set(atTop); - this.historyNavigationForewardsEnablement.set(e.position.equals(getLastPosition(model))); - } + this.historyNavigationBackwardsEnablement.set(atTop); + this.historyNavigationForewardsEnablement.set(e.position.equals(getLastPosition(model))); })); this.toolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputContainer, this.options.menus.executeToolbar, { @@ -438,31 +482,61 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.clearNode(container); this.attachedContextDisposables.clear(); dom.setVisibility(Boolean(this.attachedContext.size), this.attachedContextContainer); - for (const attachment of this.attachedContext) { + if (!this.attachedContext.size) { + this._indexOfLastAttachedContextDeletedWithKeyboard = -1; + } + [...this.attachedContext.values()].forEach((attachment, index) => { const widget = dom.append(container, $('.chat-attached-context-attachment.show-file-icons')); const label = this._contextResourceLabels.create(widget, { supportIcons: true }); const file = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; - if (file) { + const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; + if (file && attachment.isFile) { + const fileBasename = basename(file.path); + const fileDirname = dirname(file.path); + const friendlyName = `${fileBasename} ${fileDirname}`; + const ariaLabel = range ? localize('chat.fileAttachmentWithRange', "Attached file, {0}, line {1} to line {2}", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment', "Attached file, {0}", friendlyName); + label.setFile(file, { fileKind: FileKind.FILE, hidePath: true, - range: attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined, + range, }); + widget.ariaLabel = ariaLabel; + widget.tabIndex = 0; } else { - label.setLabel(attachment.fullName ?? attachment.name); + const attachmentLabel = attachment.fullName ?? attachment.name; + label.setLabel(attachmentLabel, undefined); + + widget.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); + widget.tabIndex = 0; } const clearButton = new Button(widget, { supportIcons: true }); + + // If this item is rendering in place of the last attached context item, focus the clear button so the user can continue deleting attached context items with the keyboard + if (index === Math.min(this._indexOfLastAttachedContextDeletedWithKeyboard, this.attachedContext.size - 1)) { + clearButton.focus(); + } + this.attachedContextDisposables.add(clearButton); clearButton.icon = Codicon.close; - const disp = clearButton.onDidClick(() => { + const disp = clearButton.onDidClick((e) => { this.attachedContext.delete(attachment); disp.dispose(); + + // Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click) + if (dom.isKeyboardEvent(e)) { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + this._indexOfLastAttachedContextDeletedWithKeyboard = index; + } + } + this._onDidChangeHeight.fire(); this._onDidDeleteContext.fire(attachment); }); this.attachedContextDisposables.add(disp); - } + }); } async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { @@ -475,6 +549,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (items && items.length > 0) { this.followupsDisposables.add(this.instantiationService.createInstance, ChatFollowups>(ChatFollowups, this.followupsContainer, items, this.location, undefined, followup => this._onDidAcceptFollowup.fire({ followup, response }))); } + this._onDidChangeHeight.fire(); } get contentHeight(): number { @@ -496,6 +571,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputEditorHeight = Math.min(data.inputPartEditorHeight, height - data.followupsHeight - data.inputPartVerticalPadding); + const followupsWidth = width - data.inputPartHorizontalPadding; + this.followupsContainer.style.width = `${followupsWidth}px`; + this._inputPartHeight = data.followupsHeight + inputEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight; const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); @@ -531,11 +609,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } saveState(): void { - const inputHistory = this.history.getHistory(); + const inputHistory = [...this.history]; this.historyService.saveHistory(this.location, inputHistory); } } +const historyKeyFn = (entry: IChatHistoryEntry) => JSON.stringify(entry); + function getLastPosition(model: ITextModel): IPosition { return { lineNumber: model.getLineCount(), column: model.getLineLength(model.getLineCount()) + 1 }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 66363a9abc6..fb4eae44143 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -7,41 +7,24 @@ import * as dom from 'vs/base/browser/dom'; import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { alert } from 'vs/base/browser/ui/aria/aria'; -import { Button } from 'vs/base/browser/ui/button/button'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; -import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { IAction } from 'vs/base/common/actions'; -import { distinct } from 'vs/base/common/arrays'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { coalesce, distinct } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; -import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network'; +import { FileAccess } from 'vs/base/common/network'; import { clamp } from 'vs/base/common/numbers'; import { autorun } from 'vs/base/common/observable'; -import { basename } from 'vs/base/common/path'; -import { basenameOrAuthority } from 'vs/base/common/resources'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; -import { Range } from 'vs/editor/common/core/range'; -import { TextEdit } from 'vs/editor/common/languages'; -import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; -import { IModelService } from 'vs/editor/common/services/model'; -import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; -import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { localize } from 'vs/nls'; import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; @@ -49,51 +32,53 @@ import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { FileKind, FileType } from 'vs/platform/files/common/files'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { WorkbenchCompressibleAsyncDataTree, WorkbenchList } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; -import { ChatConfirmationWidget } from 'vs/workbench/contrib/chat/browser/chatConfirmationWidget'; +import { ChatCommandButtonContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCommandContentPart'; +import { ChatConfirmationContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart'; +import { IChatContentPart, IChatContentPartRenderContext } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; +import { ChatMarkdownContentPart, EditorPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart'; +import { ChatProgressContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart'; +import { ChatReferencesContentPart, ContentReferencesListPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart'; +import { ChatTaskContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart'; +import { ChatTextEditContentPart, DiffEditorPool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart'; +import { ChatTreeContentPart, TreePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart'; +import { ChatWarningContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatWarningContentPart'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; +import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { ChatCodeBlockContentProvider, CodeBlockPart, CodeCompareBlockPart, ICodeBlockData, ICodeCompareBlockData, ICodeCompareBlockDiffData, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; +import { ChatCodeBlockContentProvider } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { ChatAgentLocation, IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatAgentVoteDirection, IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatTask, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { IChatProgressMessageRenderData, IChatRenderData, IChatResponseMarkdownRenderData, IChatResponseViewModel, IChatTaskRenderData, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; -import { IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; -import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../common/annotations'; +import { ChatAgentVoteDirection, IChatConfirmation, IChatFollowup, IChatTask, IChatTreeData } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatReferences, IChatRendererContent, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; +import { annotateSpecialMarkdownContent } from '../common/annotations'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; import { IChatListItemRendererOptions } from './chat'; -import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer'; const $ = dom.$; interface IChatListItemTemplate { currentElement?: ChatTreeItem; + renderedParts?: IChatContentPart[]; readonly rowContainer: HTMLElement; readonly titleToolbar?: MenuWorkbenchToolBar; readonly avatarContainer: HTMLElement; readonly username: HTMLElement; readonly detail: HTMLElement; readonly value: HTMLElement; - readonly referencesListContainer: HTMLElement; readonly contextKeyService: IContextKeyService; + readonly instantiationService: IInstantiationService; readonly templateDisposables: IDisposable; readonly elementDisposables: DisposableStore; readonly agentHover: ChatAgentHover; @@ -104,7 +89,9 @@ interface IItemHeightChangeParams { height: number; } -const forceVerboseLayoutTracing = false; +const forceVerboseLayoutTracing = false + // || Boolean("TRUE") // causes a linter warning so that it cannot be pushed + ; export interface IChatRendererDelegate { getListLength(): number; @@ -142,8 +129,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); - private _usedReferencesEnabled = false; - constructor( editorOptions: ChatEditorOptions, private readonly location: ChatAgentLocation, @@ -154,14 +139,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - if (e.affectsConfiguration('chat.experimental.usedReferences')) { - this._usedReferencesEnabled = configService.getValue('chat.experimental.usedReferences') ?? true; - } - })); } get templateId(): string { @@ -198,21 +172,19 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - if (isResponseVM(template.currentElement) && template.currentElement.agent) { + if (isResponseVM(template.currentElement) && template.currentElement.agent && !template.currentElement.agent.isDefault) { agentHover.setAgent(template.currentElement.agent.id); return agentHover.domNode; } @@ -311,8 +305,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer isResponseVM(template.currentElement) ? template.currentElement.agent : undefined, this.commandService); - templateDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), user, hoverContent, hoverOptions)); - templateDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), header, hoverContent, hoverOptions)); + templateDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), user, hoverContent, hoverOptions)); templateDisposables.add(dom.addDisposableListener(user, dom.EventType.KEY_DOWN, e => { const ev = new StandardKeyboardEvent(e); if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) { @@ -324,7 +317,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { try { - if (this.doNextProgressiveRender(element, index, templateData, !!initial, progressiveRenderingDisposables)) { + if (this.doNextProgressiveRender(element, index, templateData, !!initial)) { timer.cancel(); } } catch (err) { // Kill the timer if anything went wrong, avoid getting stuck in a nasty rendering loop. timer.cancel(); - throw err; + this.logService.error(err); } }; timer.cancelAndSet(runProgressiveRender, 50, dom.getWindow(templateData.rowContainer)); runProgressiveRender(true); } else if (isResponseVM(element)) { - const renderableResponse = annotateSpecialMarkdownContent(element.response.value); - this.basicRenderElement(renderableResponse, element, index, templateData); + this.basicRenderElement(element, index, templateData); } else if (isRequestVM(element)) { - const markdown = 'message' in element.message ? - element.message.message : - this.markdownDecorationsRenderer.convertParsedRequestToMarkdown(element.message); - this.basicRenderElement([{ content: new MarkdownString(markdown), kind: 'markdownContent' }], element, index, templateData); + this.basicRenderElement(element, index, templateData); } else { this.renderWelcomeMessage(element, templateData); } @@ -419,13 +406,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer, element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) { - const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete); + private basicRenderElement(element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) { + let value: IChatRendererContent[] = []; + if (isRequestVM(element)) { + const markdown = 'message' in element.message ? + element.message.message : + this.markdownDecorationsRenderer.convertParsedRequestToMarkdown(element.message); + value = [{ content: new MarkdownString(markdown), kind: 'markdownContent' }]; + } else if (isResponseVM(element)) { + value = annotateSpecialMarkdownContent(element.response.value); + if (element.contentReferences.length) { + value.unshift({ kind: 'references', references: element.contentReferences }); + } + } dom.clearNode(templateData.value); - dom.clearNode(templateData.referencesListContainer); if (isResponseVM(element)) { this.renderDetail(element, templateData); } - this.renderContentReferencesIfNeeded(element, templateData, templateData.elementDisposables); - - let fileTreeIndex = 0; + const parts: IChatContentPart[] = []; value.forEach((data, index) => { - const result = data.kind === 'treeData' - ? this.renderTreeData(data.treeData, element, templateData, fileTreeIndex++) - : data.kind === 'markdownContent' - ? this.renderMarkdown(data.content, element, templateData, fillInIncompleteTokens) - : data.kind === 'progressMessage' && onlyProgressMessagesAfterI(value, index) ? this.renderProgressMessage(data, false) // TODO render command - : data.kind === 'progressTask' ? this.renderProgressTask(data, false, element, templateData) - : data.kind === 'command' ? this.renderCommandButton(element, data) - : data.kind === 'textEditGroup' ? this.renderTextEdit(element, data, templateData) - : data.kind === 'warning' ? this.renderNotification('warning', data.content) - : data.kind === 'confirmation' ? this.renderConfirmation(element, data, templateData) - : undefined; - - if (result) { - templateData.value.appendChild(result.element); - templateData.elementDisposables.add(result); + const context: IChatContentPartRenderContext = { + element, + index, + content: value, + preceedingContentParts: parts, + }; + const newPart = this.renderChatContentPart(data, templateData, context); + if (newPart) { + templateData.value.appendChild(newPart.domNode); + parts.push(newPart); } }); + templateData.renderedParts = parts; if (isResponseVM(element) && element.errorDetails?.message) { - const renderedError = this.renderNotification(element.errorDetails.responseIsFiltered ? 'info' : 'error', new MarkdownString(element.errorDetails.message)); + const renderedError = this.instantiationService.createInstance(ChatWarningContentPart, element.errorDetails.responseIsFiltered ? 'info' : 'error', new MarkdownString(element.errorDetails.message), this.renderer); templateData.elementDisposables.add(renderedError); - templateData.value.appendChild(renderedError.element); + templateData.value.appendChild(renderedError.domNode); } const newHeight = templateData.rowContainer.offsetHeight; @@ -532,12 +519,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { if (Array.isArray(item)) { - const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, templateData.contextKeyService])); + const scopedInstaService = templateData.elementDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, templateData.contextKeyService]))); templateData.elementDisposables.add( scopedInstaService.createInstance, ChatFollowups>( ChatFollowups, @@ -547,11 +532,18 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._onDidClickFollowup.fire(followup))); } else { - const result = this.renderMarkdown(item as IMarkdownString, element, templateData); - templateData.value.appendChild(result.element); + const context: IChatContentPartRenderContext = { + element, + index: i, + // NA for welcome msg + content: [], + preceedingContentParts: [] + }; + const result = this.renderMarkdown(item, templateData, context); + templateData.value.appendChild(result.domNode); templateData.elementDisposables.add(result); } - } + }); const newHeight = templateData.rowContainer.offsetHeight; const fireEvent = !element.currentRenderedHeight || element.currentRenderedHeight !== newHeight; @@ -570,626 +562,144 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - const renderedPart = renderedParts[index]; - // Is this part completely new? - if (!renderedPart) { - if (part.kind === 'treeData') { - partsToRender[index] = part.treeData; - } else if (part.kind === 'progressMessage') { - partsToRender[index] = { - progressMessage: part, - isAtEndOfResponse: onlyProgressMessagesAfterI(renderableResponse, index), - isLast: index === renderableResponse.length - 1, - } satisfies IChatProgressMessageRenderData; - } else if (part.kind === 'command' || - part.kind === 'textEditGroup' || - part.kind === 'confirmation' || - part.kind === 'warning') { - partsToRender[index] = part; - } else if (part.kind === 'progressTask') { - partsToRender[index] = { - task: part, - isSettled: part.isSettled?.() ?? true, - progressLength: part.progress.length, - }; - } else { - const wordCountResult = this.getDataForProgressiveRender(element, contentToMarkdown(part.content), { renderedWordCount: 0, lastRenderTime: 0 }); - if (wordCountResult !== undefined) { - this.traceLayout('doNextProgressiveRender', `Rendering new part ${index}, wordCountResult=${wordCountResult.actualWordCount}, rate=${wordCountResult.rate}`); - partsToRender[index] = { - renderedWordCount: wordCountResult.actualWordCount, - lastRenderTime: Date.now(), - isFullyRendered: wordCountResult.isFullString, - originalMarkdown: part.content, - }; - wordCountResults[index] = wordCountResult; - } - } - } - - // Did this part's content change? - else if ((part.kind === 'markdownContent' || part.kind === 'progressMessage') && isMarkdownRenderData(renderedPart)) { // TODO - const wordCountResult = this.getDataForProgressiveRender(element, contentToMarkdown(part.content), renderedPart); - // Check if there are any new words to render - if (wordCountResult !== undefined && renderedPart.renderedWordCount !== wordCountResult?.actualWordCount) { - this.traceLayout('doNextProgressiveRender', `Rendering changed part ${index}, wordCountResult=${wordCountResult.actualWordCount}, rate=${wordCountResult.rate}`); - partsToRender[index] = { - renderedWordCount: wordCountResult.actualWordCount, - lastRenderTime: Date.now(), - isFullyRendered: wordCountResult.isFullString, - originalMarkdown: part.content, - }; - wordCountResults[index] = wordCountResult; - } else if (!renderedPart.isFullyRendered && !wordCountResult) { - // This part is not fully rendered, but not enough time has passed to render more content - somePartIsNotFullyRendered = true; - } - } - - // Is it a progress message that needs to be rerendered? - else if (part.kind === 'progressMessage' && isProgressMessageRenderData(renderedPart) && ( - (renderedPart.isAtEndOfResponse !== onlyProgressMessagesAfterI(renderableResponse, index)) || - renderedPart.isLast !== (index === renderableResponse.length - 1))) { - partsToRender[index] = { - progressMessage: part, - isAtEndOfResponse: onlyProgressMessagesAfterI(renderableResponse, index), - isLast: index === renderableResponse.length - 1, - } satisfies IChatProgressMessageRenderData; - } - - else if (part.kind === 'progressTask' && isProgressTaskRenderData(renderedPart)) { - const isSettled = part.isSettled?.() ?? true; - if (renderedPart.isSettled !== isSettled || part.progress.length !== renderedPart.progressLength || isSettled) { - partsToRender[index] = { task: part, isSettled, progressLength: part.progress.length }; - } - } - }); - - isFullyRendered = partsToRender.filter((p) => !('isSettled' in p) || !p.isSettled).length === 0 && !somePartIsNotFullyRendered; - - if (isFullyRendered && element.isComplete) { - // Response is done and content is rendered, so do a normal render - this.traceLayout('runProgressiveRender', `end progressive render, index=${index} and clearing renderData, response is complete, index=${index}`); - element.renderData = undefined; - disposables.clear(); - this.basicRenderElement(renderableResponse, element, index, templateData); - } else if (!isFullyRendered) { - disposables.clear(); - this.renderContentReferencesIfNeeded(element, templateData, disposables); - let hasRenderedOneMarkdownBlock = false; - partsToRender.forEach((partToRender, index) => { - if (!partToRender) { - return; - } - - // Undefined => don't do anything. null => remove the rendered element - let result: { element: HTMLElement } & IDisposable | undefined | null; - if (isInteractiveProgressTreeData(partToRender)) { - result = this.renderTreeData(partToRender, element, templateData, index); - } else if (isProgressMessageRenderData(partToRender)) { - if (onlyProgressMessageRenderDatasAfterI(partsToRender, index)) { - result = this.renderProgressMessage(partToRender.progressMessage, index === partsToRender.length - 1); - } else { - result = null; - } - } else if (isProgressTaskRenderData(partToRender)) { - result = this.renderProgressTask(partToRender.task, !partToRender.isSettled, element, templateData); - } else if (isCommandButtonRenderData(partToRender)) { - result = this.renderCommandButton(element, partToRender); - } else if (isTextEditRenderData(partToRender)) { - result = this.renderTextEdit(element, partToRender, templateData); - } else if (isConfirmationRenderData(partToRender)) { - result = this.renderConfirmation(element, partToRender, templateData); - } else if (isWarningRenderData(partToRender)) { - result = this.renderNotification('warning', partToRender.content); - } - - // Avoid doing progressive rendering for multiple markdown parts simultaneously - else if (!hasRenderedOneMarkdownBlock && wordCountResults[index]) { - const { value } = wordCountResults[index]; - const part = partsToRender[index]; - const originalMarkdown = 'originalMarkdown' in part ? part.originalMarkdown : undefined; - const markdownToRender = new MarkdownString(value, originalMarkdown); - result = this.renderMarkdown(markdownToRender, element, templateData, true); - hasRenderedOneMarkdownBlock = true; - } - - if (result === undefined) { - return; - } - - // Doing the progressive render - renderedParts[index] = partToRender; - const existingElement = templateData.value.children[index]; - if (existingElement) { - if (result === null) { - templateData.value.replaceChild($('span.placeholder-for-deleted-thing'), existingElement); - } else { - templateData.value.replaceChild(result.element, existingElement); - } - } else if (result) { - templateData.value.appendChild(result.element); - } - - if (result) { - disposables.add(result); - } - }); - } else { - // Nothing new to render, not done, keep waiting - return false; - } + this.basicRenderElement(element, index, templateData); + return true; } - // Some render happened - update the height + let isFullyRendered = false; + this.traceLayout('doNextProgressiveRender', `START progressive render, index=${index}, renderData=${JSON.stringify(element.renderData)}`); + const contentForThisTurn = this.getNextProgressiveRenderContent(element); + const partsToRender = this.diff(templateData.renderedParts ?? [], contentForThisTurn, element); + isFullyRendered = partsToRender.every(part => part === null); + + if (isFullyRendered) { + if (element.isComplete) { + // Response is done and content is rendered, so do a normal render + this.traceLayout('doNextProgressiveRender', `END progressive render, index=${index} and clearing renderData, response is complete`); + element.renderData = undefined; + this.basicRenderElement(element, index, templateData); + return true; + } + + // Nothing new to render, not done, keep waiting + this.traceLayout('doNextProgressiveRender', 'caught up with the stream- no new content to render'); + return false; + } + + // Do an actual progressive render + this.traceLayout('doNextProgressiveRender', `doing progressive render, ${partsToRender.length} parts to render`); + this.renderChatContentDiff(partsToRender, contentForThisTurn, element, templateData); + const height = templateData.rowContainer.offsetHeight; element.currentRenderedHeight = height; if (!isInRenderElement) { this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); } - return isFullyRendered; + return false; } - private renderTreeData(data: IChatResponseProgressFileTreeData, element: ChatTreeItem, templateData: IChatListItemTemplate, treeDataIndex: number): { element: HTMLElement; dispose: () => void } { - const treeDisposables = new DisposableStore(); - const ref = treeDisposables.add(this._treePool.get()); - const tree = ref.object; - - treeDisposables.add(tree.onDidOpen((e) => { - if (e.element && !('children' in e.element)) { - this.openerService.open(e.element.uri); + private renderChatContentDiff(partsToRender: ReadonlyArray, contentForThisTurn: ReadonlyArray, element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { + const renderedParts = templateData.renderedParts ?? []; + templateData.renderedParts = renderedParts; + partsToRender.forEach((partToRender, index) => { + if (!partToRender) { + // null=no change + return; } - })); - treeDisposables.add(tree.onDidChangeCollapseState(() => { - this.updateItemHeight(templateData); - })); - treeDisposables.add(tree.onContextMenu((e) => { - e.browserEvent.preventDefault(); - e.browserEvent.stopPropagation(); - })); - tree.setInput(data).then(() => { - if (!ref.isStale()) { - tree.layout(); - this.updateItemHeight(templateData); + const alreadyRenderedPart = templateData.renderedParts?.[index]; + if (alreadyRenderedPart) { + alreadyRenderedPart.dispose(); } - }); - if (isResponseVM(element)) { - const fileTreeFocusInfo = { - treeDataId: data.uri.toString(), - treeIndex: treeDataIndex, - focus() { - tree.domFocus(); - } + const preceedingContentParts = renderedParts.slice(0, index); + const context: IChatContentPartRenderContext = { + element, + content: contentForThisTurn, + preceedingContentParts, + index }; - - treeDisposables.add(tree.onDidFocus(() => { - this.focusedFileTreesByResponseId.set(element.id, fileTreeFocusInfo.treeIndex); - })); - - const fileTrees = this.fileTreesByResponseId.get(element.id) ?? []; - fileTrees.push(fileTreeFocusInfo); - this.fileTreesByResponseId.set(element.id, distinct(fileTrees, (v) => v.treeDataId)); - treeDisposables.add(toDisposable(() => this.fileTreesByResponseId.set(element.id, fileTrees.filter(v => v.treeDataId !== data.uri.toString())))); - } - - return { - element: tree.getHTMLElement().parentElement!, - dispose: () => { - treeDisposables.dispose(); - } - }; - } - - private renderContentReferencesIfNeeded(element: ChatTreeItem, templateData: IChatListItemTemplate, disposables: DisposableStore): void { - if (isResponseVM(element) && this._usedReferencesEnabled && element.contentReferences.length) { - dom.show(templateData.referencesListContainer); - const contentReferencesListResult = this.renderContentReferencesListData(null, element.contentReferences, element, templateData); - if (templateData.referencesListContainer.firstChild) { - templateData.referencesListContainer.replaceChild(contentReferencesListResult.element, templateData.referencesListContainer.firstChild!); - } else { - templateData.referencesListContainer.appendChild(contentReferencesListResult.element); - } - disposables.add(contentReferencesListResult); - } else { - dom.hide(templateData.referencesListContainer); - } - } - - private renderContentReferencesListData(task: IChatTask | null, data: ReadonlyArray, element: IChatResponseViewModel, templateData: IChatListItemTemplate): { element: HTMLElement; dispose: () => void } { - const listDisposables = new DisposableStore(); - const referencesLabel = task?.content.value ?? (data.length > 1 ? - localize('usedReferencesPlural', "Used {0} references", data.length) : - localize('usedReferencesSingular', "Used {0} reference", 1)); - const iconElement = $('.chat-used-context-icon'); - const icon = (element: IChatResponseViewModel) => element.usedReferencesExpanded ? Codicon.chevronDown : Codicon.chevronRight; - iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); - const buttonElement = $('.chat-used-context-label', undefined); - - const collapseButton = listDisposables.add(new Button(buttonElement, { - buttonBackground: undefined, - buttonBorder: undefined, - buttonForeground: undefined, - buttonHoverBackground: undefined, - buttonSecondaryBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryHoverBackground: undefined, - buttonSeparator: undefined - })); - const container = $('.chat-used-context', undefined, buttonElement); - collapseButton.label = referencesLabel; - collapseButton.element.prepend(iconElement); - this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); - container.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); - listDisposables.add(collapseButton.onDidClick(() => { - iconElement.classList.remove(...ThemeIcon.asClassNameArray(icon(element))); - element.usedReferencesExpanded = !element.usedReferencesExpanded; - iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); - container.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); - this.updateItemHeight(templateData); - this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); - })); - - const ref = listDisposables.add(this._contentReferencesListPool.get()); - const list = ref.object; - container.appendChild(list.getHTMLElement().parentElement!); - - listDisposables.add(list.onDidOpen((e) => { - if (e.element && 'reference' in e.element) { - const uriOrLocation = 'variableName' in e.element.reference ? e.element.reference.value : e.element.reference; - const uri = URI.isUri(uriOrLocation) ? uriOrLocation : - uriOrLocation?.uri; - if (uri) { - this.openerService.open( - uri, - { - fromUserGesture: true, - editorOptions: { - ...e.editorOptions, - ...{ - selection: uriOrLocation && 'range' in uriOrLocation ? uriOrLocation.range : undefined - } - } - }); - } - } - })); - listDisposables.add(list.onContextMenu((e) => { - e.browserEvent.preventDefault(); - e.browserEvent.stopPropagation(); - })); - - const maxItemsShown = 6; - const itemsShown = Math.min(data.length, maxItemsShown); - const height = itemsShown * 22; - list.layout(height); - list.getHTMLElement().style.height = `${height}px`; - list.splice(0, list.length, data); - - return { - element: container, - dispose: () => { - listDisposables.dispose(); - } - }; - } - - private updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { - element.ariaLabel = expanded ? localize('usedReferencesExpanded', "{0}, expanded", label) : localize('usedReferencesCollapsed', "{0}, collapsed", label); - } - - private renderProgressTask(task: IChatTask, showSpinner: boolean, element: ChatTreeItem, templateData: IChatListItemTemplate): IMarkdownRenderResult | undefined { - if (!isResponseVM(element)) { - return; - } - - if (task.progress.length) { - const refs = this.renderContentReferencesListData(task, task.progress, element, templateData); - const node = dom.$('.chat-progress-task'); - node.appendChild(refs.element); - return { element: node, dispose: refs.dispose }; - } - - return this.renderProgressMessage(task, showSpinner); - } - - private renderProgressMessage(progress: IChatProgressMessage | IChatTask, showSpinner: boolean): IMarkdownRenderResult { - if (showSpinner) { - // this step is in progress, communicate it to SR users - alert(progress.content.value); - } - const codicon = showSpinner ? ThemeIcon.modify(Codicon.loading, 'spin').id : Codicon.check.id; - const markdown = new MarkdownString(`$(${codicon}) ${progress.content.value}`, { - supportThemeIcons: true - }); - const result = this.renderer.render(markdown); - result.element.classList.add('progress-step'); - return result; - } - - private renderCommandButton(element: ChatTreeItem, commandButton: IChatCommandButton): IMarkdownRenderResult { - const container = $('.chat-command-button'); - const disposables = new DisposableStore(); - const enabled = !isResponseVM(element) || !element.isStale; - const tooltip = enabled ? - commandButton.command.tooltip : - localize('commandButtonDisabled', "Button not available in restored chat"); - const button = disposables.add(new Button(container, { ...defaultButtonStyles, supportIcons: true, title: tooltip })); - button.label = commandButton.command.title; - button.enabled = enabled; - - // TODO still need telemetry for command buttons - disposables.add(button.onDidClick(() => this.commandService.executeCommand(commandButton.command.id, ...(commandButton.command.arguments ?? [])))); - return { - dispose() { - disposables.dispose(); - }, - element: container - }; - } - - private renderNotification(kind: 'info' | 'warning' | 'error', content: IMarkdownString): IMarkdownRenderResult { - const container = $('.chat-notification-widget'); - let icon; - let iconClass; - switch (kind) { - case 'warning': - icon = Codicon.warning; - iconClass = '.chat-warning-codicon'; - break; - case 'error': - icon = Codicon.error; - iconClass = '.chat-error-codicon'; - break; - case 'info': - icon = Codicon.info; - iconClass = '.chat-info-codicon'; - break; - } - container.appendChild($(iconClass, undefined, renderIcon(icon))); - const markdownContent = this.renderer.render(content); - container.appendChild(markdownContent.element); - return { - element: container, - dispose() { markdownContent.dispose(); } - }; - } - - private renderConfirmation(element: ChatTreeItem, confirmation: IChatConfirmation, templateData: IChatListItemTemplate): IMarkdownRenderResult | undefined { - const store = new DisposableStore(); - const confirmationWidget = store.add(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, [ - { label: localize('accept', "Accept"), data: confirmation.data }, - { label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true }, - ])); - confirmationWidget.setShowButtons(!confirmation.isUsed); - - store.add(confirmationWidget.onDidClick(async e => { - if (isResponseVM(element)) { - const prompt = `${e.label}: "${confirmation.title}"`; - const data: IChatSendRequestOptions = e.isSecondary ? - { rejectedConfirmationData: [e.data] } : - { acceptedConfirmationData: [e.data] }; - data.agentId = element.agent?.id; - if (await this.chatService.sendRequest(element.sessionId, prompt, data)) { - confirmation.isUsed = true; - confirmationWidget.setShowButtons(false); - this.updateItemHeight(templateData); - } - } - })); - - return { - element: confirmationWidget.domNode, - dispose() { store.dispose(); } - }; - } - - private renderTextEdit(element: ChatTreeItem, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IMarkdownRenderResult | undefined { - - // TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen - if (this.rendererOptions.renderTextEditsAsSummary?.(chatTextEdit.uri)) { - if (isResponseVM(element) && element.response.value.every(item => item.kind === 'textEditGroup')) { - return { - element: $('.interactive-edits-summary', undefined, !element.isComplete ? localize('editsSummary1', "Making changes...") : localize('editsSummary', "Made changes.")), - dispose() { } - }; - } - return undefined; - } - - const store = new DisposableStore(); - const cts = new CancellationTokenSource(); - - let isDisposed = false; - store.add(toDisposable(() => { - isDisposed = true; - cts.dispose(true); - })); - - const ref = this._diffEditorPool.get(); - - // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) - // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) - store.add(ref.object.onDidChangeContentHeight(() => { - ref.object.layout(this._currentLayoutWidth); - this.updateItemHeight(templateData); - })); - - const data: ICodeCompareBlockData = { - element, - edit: chatTextEdit, - diffData: (async () => { - - const ref = await this.textModelService.createModelReference(chatTextEdit.uri); - - if (isDisposed) { - ref.dispose(); - return; - } - - store.add(ref); - - const original = ref.object.textEditorModel; - let originalSha1: string = ''; - - if (chatTextEdit.state) { - originalSha1 = chatTextEdit.state.sha1; - } else { - const sha1 = new DefaultModelSHA1Computer(); - if (sha1.canComputeSHA1(original)) { - originalSha1 = sha1.computeSHA1(original); - chatTextEdit.state = { sha1: originalSha1, applied: 0 }; - } - } - - const modified = this.modelService.createModel( - createTextBufferFactoryFromSnapshot(original.createSnapshot()), - { languageId: original.getLanguageId(), onDidChange: Event.None }, - URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: original.uri.path, query: generateUuid() }), - false - ); - store.add(modified); - if (!chatTextEdit.state?.applied) { - for (const group of chatTextEdit.edits) { - const edits = group.map(TextEdit.asEditOperation); - modified.pushEditOperations(null, edits, () => null); - } - } - return { - modified, - original, - originalSha1 - } satisfies ICodeCompareBlockDiffData; - })() - }; - ref.object.render(data, this._currentLayoutWidth, cts.token); - - return { - element: ref.object.element, - dispose() { - store.dispose(); - }, - }; - } - - private renderMarkdown(markdown: IMarkdownString, element: ChatTreeItem, templateData: IChatListItemTemplate, fillInIncompleteTokens = false): IMarkdownRenderResult { - const disposables = new DisposableStore(); - - // We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering - const orderedDisposablesList: IDisposable[] = []; - const codeblocks: IChatCodeBlockInfo[] = []; - let codeBlockIndex = 0; - const result = this.renderer.render(markdown, { - fillInIncompleteTokens, - codeBlockRendererSync: (languageId, text) => { - const index = codeBlockIndex++; - let textModel: Promise; - let range: Range | undefined; - let vulns: readonly IMarkdownVulnerability[] | undefined; - if (equalsIgnoreCase(languageId, localFileLanguageId)) { + const newPart = this.renderChatContentPart(partToRender, templateData, context); + if (newPart) { + // Maybe the part can't be rendered in this context, but this shouldn't really happen + if (alreadyRenderedPart) { try { - const parsedBody = parseLocalFileData(text); - range = parsedBody.range && Range.lift(parsedBody.range); - textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object); - } catch (e) { - return $('div'); + // This method can throw HierarchyRequestError + alreadyRenderedPart.domNode.replaceWith(newPart.domNode); + } catch (err) { + this.logService.error('ChatListItemRenderer#renderChatContentDiff: error replacing part', err); } } else { - if (!isRequestVM(element) && !isResponseVM(element)) { - console.error('Trying to render code block in welcome', element.id, index); - return $('div'); - } - - const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; - const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); - vulns = modelEntry.vulns; - textModel = modelEntry.model; + templateData.value.appendChild(newPart.domNode); } - const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; - const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: templateData.contextKeyService, vulns }, text); - - // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) - // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) - disposables.add(ref.object.onDidChangeContentHeight(() => { - ref.object.layout(this._currentLayoutWidth); - this.updateItemHeight(templateData); - })); - - if (isResponseVM(element)) { - const info: IChatCodeBlockInfo = { - codeBlockIndex: index, - element, - focus() { - ref.object.focus(); - } - }; - codeblocks.push(info); - if (ref.object.uri) { - const uri = ref.object.uri; - this.codeBlocksByEditorUri.set(uri, info); - disposables.add(toDisposable(() => this.codeBlocksByEditorUri.delete(uri))); - } - } - orderedDisposablesList.push(ref); - return ref.object.element; - }, - asyncRenderCallback: () => this.updateItemHeight(templateData), - }); - - if (isResponseVM(element)) { - this.codeBlocksByResponseId.set(element.id, codeblocks); - disposables.add(toDisposable(() => this.codeBlocksByResponseId.delete(element.id))); - } - - disposables.add(this.markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(result.element)); - - orderedDisposablesList.reverse().forEach(d => disposables.add(d)); - return { - element: result.element, - dispose() { - result.dispose(); - disposables.dispose(); + renderedParts[index] = newPart; + } else if (alreadyRenderedPart) { + alreadyRenderedPart.domNode.remove(); } - }; + }); } - private renderCodeBlock(data: ICodeBlockData, text: string): IDisposableReference { - const ref = this._editorPool.get(); - const editorInfo = ref.object; - if (isResponseVM(data.element)) { - this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }); + /** + * Returns all content parts that should be rendered, and trimmed markdown content. We will diff this with the current rendered set. + */ + private getNextProgressiveRenderContent(element: IChatResponseViewModel): IChatRendererContent[] { + const data = this.getDataForProgressiveRender(element); + + const renderableResponse = annotateSpecialMarkdownContent(element.response.value); + + this.traceLayout('getNextProgressiveRenderContent', `Want to render ${data.numWordsToRender} at ${data.rate} words/s, counting...`); + let numNeededWords = data.numWordsToRender; + const partsToRender: IChatRendererContent[] = []; + if (element.contentReferences.length) { + partsToRender.push({ kind: 'references', references: element.contentReferences }); } - editorInfo.render(data, this._currentLayoutWidth, this.rendererOptions.editableCodeBlock); + for (let i = 0; i < renderableResponse.length; i++) { + const part = renderableResponse[i]; + if (numNeededWords <= 0) { + break; + } - return ref; + if (part.kind === 'markdownContent') { + const wordCountResult = getNWords(part.content.value, numNeededWords); + if (wordCountResult.isFullString) { + partsToRender.push(part); + } else { + partsToRender.push({ kind: 'markdownContent', content: new MarkdownString(wordCountResult.value, part.content) }); + } + + this.traceLayout('getNextProgressiveRenderContent', ` Chunk ${i}: Want to render ${numNeededWords} words and found ${wordCountResult.returnedWordCount} words. Total words in chunk: ${wordCountResult.totalWordCount}`); + numNeededWords -= wordCountResult.returnedWordCount; + } else { + partsToRender.push(part); + } + } + + const lastWordCount = element.contentUpdateTimings?.lastWordCount ?? 0; + const newRenderedWordCount = data.numWordsToRender - numNeededWords; + const bufferWords = lastWordCount - newRenderedWordCount; + this.traceLayout('getNextProgressiveRenderContent', `Want to render ${data.numWordsToRender} words. Rendering ${newRenderedWordCount} words. Buffer: ${bufferWords} words`); + if (newRenderedWordCount > 0 && newRenderedWordCount !== element.renderData?.renderedWordCount) { + // Only update lastRenderTime when we actually render new content + element.renderData = { lastRenderTime: Date.now(), renderedWordCount: newRenderedWordCount, renderedParts: partsToRender }; + } + + return partsToRender; } - private getDataForProgressiveRender(element: IChatResponseViewModel, data: IMarkdownString, renderData: Pick): IWordCountResult & { rate: number } | undefined { + private getDataForProgressiveRender(element: IChatResponseViewModel) { + const renderData = element.renderData ?? { lastRenderTime: 0, renderedWordCount: 0 }; + const rate = this.getProgressiveRenderRate(element); const numWordsToRender = renderData.lastRenderTime === 0 ? 1 : @@ -1197,17 +707,168 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer, contentToRender: ReadonlyArray, element: ChatTreeItem): ReadonlyArray { + const diff: (IChatRendererContent | null)[] = []; + for (let i = 0; i < contentToRender.length; i++) { + const content = contentToRender[i]; + const renderedPart = renderedParts[i]; + + if (!renderedPart || !renderedPart.hasSameContent(content, contentToRender.slice(i + 1), element)) { + diff.push(content); + } else { + // null -> no change + diff.push(null); + } + } + + return diff; + } + + private renderChatContentPart(content: IChatRendererContent, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { + if (content.kind === 'treeData') { + return this.renderTreeData(content, templateData, context); + } else if (content.kind === 'progressMessage') { + return this.instantiationService.createInstance(ChatProgressContentPart, content, this.renderer, context); + } else if (content.kind === 'progressTask') { + return this.renderProgressTask(content, templateData, context); + } else if (content.kind === 'command') { + return this.instantiationService.createInstance(ChatCommandButtonContentPart, content, context); + } else if (content.kind === 'textEditGroup') { + return this.renderTextEdit(context, content, templateData); + } else if (content.kind === 'confirmation') { + return this.renderConfirmation(context, content, templateData); + } else if (content.kind === 'warning') { + return this.instantiationService.createInstance(ChatWarningContentPart, 'warning', content.content, this.renderer); + } else if (content.kind === 'markdownContent') { + return this.renderMarkdown(content.content, templateData, context); + } else if (content.kind === 'references') { + return this.renderContentReferencesListData(content, undefined, context, templateData); + } + + return undefined; + } + + private renderTreeData(content: IChatTreeData, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { + const data = content.treeData; + const treeDataIndex = context.preceedingContentParts.filter(part => part instanceof ChatTreeContentPart).length; + const treePart = this.instantiationService.createInstance(ChatTreeContentPart, data, context.element, this._treePool, treeDataIndex); + + treePart.addDisposable(treePart.onDidChangeHeight(() => { + this.updateItemHeight(templateData); + })); + + if (isResponseVM(context.element)) { + const fileTreeFocusInfo = { + treeDataId: data.uri.toString(), + treeIndex: treeDataIndex, + focus() { + treePart.domFocus(); + } + }; + + // TODO@roblourens there's got to be a better way to navigate trees + treePart.addDisposable(treePart.onDidFocus(() => { + this.focusedFileTreesByResponseId.set(context.element.id, fileTreeFocusInfo.treeIndex); + })); + + const fileTrees = this.fileTreesByResponseId.get(context.element.id) ?? []; + fileTrees.push(fileTreeFocusInfo); + this.fileTreesByResponseId.set(context.element.id, distinct(fileTrees, (v) => v.treeDataId)); + treePart.addDisposable(toDisposable(() => this.fileTreesByResponseId.set(context.element.id, fileTrees.filter(v => v.treeDataId !== data.uri.toString())))); + } + + return treePart; + } + + private renderContentReferencesListData(references: IChatReferences, labelOverride: string | undefined, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): ChatReferencesContentPart { + const referencesPart = this.instantiationService.createInstance(ChatReferencesContentPart, references.references, labelOverride, context.element as IChatResponseViewModel, this._contentReferencesListPool); + referencesPart.addDisposable(referencesPart.onDidChangeHeight(() => { + this.updateItemHeight(templateData); + })); + + return referencesPart; + } + + private renderProgressTask(task: IChatTask, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { + if (!isResponseVM(context.element)) { + return; + } + + const taskPart = this.instantiationService.createInstance(ChatTaskContentPart, task, this._contentReferencesListPool, this.renderer, context); + taskPart.addDisposable(taskPart.onDidChangeHeight(() => { + this.updateItemHeight(templateData); + })); + return taskPart; + } + + private renderConfirmation(context: IChatContentPartRenderContext, confirmation: IChatConfirmation, templateData: IChatListItemTemplate): IChatContentPart { + const part = this.instantiationService.createInstance(ChatConfirmationContentPart, confirmation, context); + part.addDisposable(part.onDidChangeHeight(() => this.updateItemHeight(templateData))); + return part; + } + + private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { + const textEditPart = this.instantiationService.createInstance(ChatTextEditContentPart, chatTextEdit, context, this.rendererOptions, this._diffEditorPool, this._currentLayoutWidth); + textEditPart.addDisposable(textEditPart.onDidChangeHeight(() => { + textEditPart.layout(this._currentLayoutWidth); + this.updateItemHeight(templateData); + })); + + return textEditPart; + } + + private renderMarkdown(markdown: IMarkdownString, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { + const element = context.element; + const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete || !!element.renderData); + const codeBlockStartIndex = context.preceedingContentParts.reduce((acc, part) => acc + (part instanceof ChatMarkdownContentPart ? part.codeblocks.length : 0), 0); + const markdownPart = this.instantiationService.createInstance(ChatMarkdownContentPart, markdown, context, this._editorPool, fillInIncompleteTokens, codeBlockStartIndex, this.renderer, this._currentLayoutWidth, this.codeBlockModelCollection, this.rendererOptions); + markdownPart.addDisposable(markdownPart.onDidChangeHeight(() => { + markdownPart.layout(this._currentLayoutWidth); + this.updateItemHeight(templateData); + })); + + const codeBlocksByResponseId = this.codeBlocksByResponseId.get(element.id) ?? []; + this.codeBlocksByResponseId.set(element.id, codeBlocksByResponseId); + markdownPart.addDisposable(toDisposable(() => { + const codeBlocksByResponseId = this.codeBlocksByResponseId.get(element.id); + if (codeBlocksByResponseId) { + markdownPart.codeblocks.forEach((info, i) => delete codeBlocksByResponseId[codeBlockStartIndex + i]); + } + })); + + markdownPart.codeblocks.forEach((info, i) => { + codeBlocksByResponseId[codeBlockStartIndex + i] = info; + if (info.uri) { + const uri = info.uri; + this.codeBlocksByEditorUri.set(uri, info); + markdownPart.addDisposable(toDisposable(() => this.codeBlocksByEditorUri.delete(uri))); + } + }); + + return markdownPart; + } + disposeElement(node: ITreeNode, index: number, templateData: IChatListItemTemplate): void { + this.traceLayout('disposeElement', `Disposing element, index=${index}`); + + // We could actually reuse a template across a renderElement call? + if (templateData.renderedParts) { + try { + dispose(coalesce(templateData.renderedParts)); + templateData.renderedParts = undefined; + dom.clearNode(templateData.value); + } catch (err) { + throw err; + } + } + + templateData.currentElement = undefined; templateData.elementDisposables.clear(); } @@ -1246,469 +907,9 @@ export class ChatListDelegate implements IListVirtualDelegate { } } - -interface IDisposableReference extends IDisposable { - object: T; - isStale: () => boolean; -} - -class EditorPool extends Disposable { - - private readonly _pool: ResourcePool; - - public inUse(): Iterable { - return this._pool.inUse; - } - - constructor( - options: ChatEditorOptions, - delegate: IChatRendererDelegate, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => { - return instantiationService.createInstance(CodeBlockPart, options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode); - })); - } - - get(): IDisposableReference { - const codeBlock = this._pool.get(); - let stale = false; - return { - object: codeBlock, - isStale: () => stale, - dispose: () => { - codeBlock.reset(); - stale = true; - this._pool.release(codeBlock); - } - }; - } -} - -class DiffEditorPool extends Disposable { - - private readonly _pool: ResourcePool; - - public inUse(): Iterable { - return this._pool.inUse; - } - - constructor( - options: ChatEditorOptions, - delegate: IChatRendererDelegate, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => { - return instantiationService.createInstance(CodeCompareBlockPart, options, MenuId.ChatCompareBlock, delegate, overflowWidgetsDomNode); - })); - } - - get(): IDisposableReference { - const codeBlock = this._pool.get(); - let stale = false; - return { - object: codeBlock, - isStale: () => stale, - dispose: () => { - codeBlock.reset(); - stale = true; - this._pool.release(codeBlock); - } - }; - } -} - -class TreePool extends Disposable { - private _pool: ResourcePool>; - - public get inUse(): ReadonlySet> { - return this._pool.inUse; - } - - constructor( - private _onDidChangeVisibility: Event, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IConfigurationService private readonly configService: IConfigurationService, - @IThemeService private readonly themeService: IThemeService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => this.treeFactory())); - } - - private treeFactory(): WorkbenchCompressibleAsyncDataTree { - const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); - - const container = $('.interactive-response-progress-tree'); - this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - - const tree = this.instantiationService.createInstance( - WorkbenchCompressibleAsyncDataTree, - 'ChatListRenderer', - container, - new ChatListTreeDelegate(), - new ChatListTreeCompressionDelegate(), - [new ChatListTreeRenderer(resourceLabels, this.configService.getValue('explorer.decorations'))], - new ChatListTreeDataSource(), - { - collapseByDefault: () => false, - expandOnlyOnTwistieClick: () => false, - identityProvider: { - getId: (e: IChatResponseProgressFileTreeData) => e.uri.toString() - }, - accessibilityProvider: { - getAriaLabel: (element: IChatResponseProgressFileTreeData) => element.label, - getWidgetAriaLabel: () => localize('treeAriaLabel', "File Tree") - }, - alwaysConsumeMouseWheel: false - }); - - return tree; - } - - get(): IDisposableReference> { - const object = this._pool.get(); - let stale = false; - return { - object, - isStale: () => stale, - dispose: () => { - stale = true; - this._pool.release(object); - } - }; - } -} - -class ContentReferencesListPool extends Disposable { - private _pool: ResourcePool>; - - public get inUse(): ReadonlySet> { - return this._pool.inUse; - } - - constructor( - private _onDidChangeVisibility: Event, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IThemeService private readonly themeService: IThemeService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => this.listFactory())); - } - - private listFactory(): WorkbenchList { - const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); - - const container = $('.chat-used-context-list'); - this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - - const list = this.instantiationService.createInstance( - WorkbenchList, - 'ChatListRenderer', - container, - new ContentReferencesListDelegate(), - [this.instantiationService.createInstance(ContentReferencesListRenderer, resourceLabels)], - { - alwaysConsumeMouseWheel: false, - accessibilityProvider: { - getAriaLabel: (element: IChatContentReference | IChatWarningMessage) => { - if (element.kind === 'warning') { - return element.content.value; - } - const reference = element.reference; - if ('variableName' in reference) { - return reference.variableName; - } else if (URI.isUri(reference)) { - return basename(reference.path); - } else { - return basename(reference.uri.path); - } - }, - - getWidgetAriaLabel: () => localize('usedReferences', "Used References") - }, - dnd: { - getDragURI: (element: IChatContentReference | IChatWarningMessage) => { - if (element.kind === 'warning') { - return null; - } - const { reference } = element; - if ('variableName' in reference) { - return null; - } else if (URI.isUri(reference)) { - return reference.toString(); - } else { - return reference.uri.toString(); - } - }, - dispose: () => { }, - onDragOver: () => false, - drop: () => { }, - }, - }); - - return list; - } - - get(): IDisposableReference> { - const object = this._pool.get(); - let stale = false; - return { - object, - isStale: () => stale, - dispose: () => { - stale = true; - this._pool.release(object); - } - }; - } -} - -class ContentReferencesListDelegate implements IListVirtualDelegate { - getHeight(element: IChatContentReference): number { - return 22; - } - - getTemplateId(element: IChatContentReference): string { - return ContentReferencesListRenderer.TEMPLATE_ID; - } -} - -interface IChatContentReferenceListTemplate { - label: IResourceLabel; - templateDisposables: IDisposable; -} - -class ContentReferencesListRenderer implements IListRenderer { - static TEMPLATE_ID = 'contentReferencesListRenderer'; - readonly templateId: string = ContentReferencesListRenderer.TEMPLATE_ID; - - constructor( - private labels: ResourceLabels, - @IThemeService private readonly themeService: IThemeService, - @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, - ) { } - - renderTemplate(container: HTMLElement): IChatContentReferenceListTemplate { - const templateDisposables = new DisposableStore(); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); - return { templateDisposables, label }; - } - - - private getReferenceIcon(data: IChatContentReference): URI | ThemeIcon | undefined { - if (ThemeIcon.isThemeIcon(data.iconPath)) { - return data.iconPath; - } else { - return this.themeService.getColorTheme().type === ColorScheme.DARK && data.iconPath?.dark - ? data.iconPath?.dark - : data.iconPath?.light; - } - } - - renderElement(data: IChatContentReference | IChatWarningMessage, index: number, templateData: IChatContentReferenceListTemplate, height: number | undefined): void { - if (data.kind === 'warning') { - templateData.label.setResource({ name: data.content.value }, { icon: Codicon.warning }); - return; - } - - const reference = data.reference; - const icon = this.getReferenceIcon(data); - templateData.label.element.style.display = 'flex'; - if ('variableName' in reference) { - if (reference.value) { - const uri = URI.isUri(reference.value) ? reference.value : reference.value.uri; - templateData.label.setResource( - { - resource: uri, - name: basenameOrAuthority(uri), - description: `#${reference.variableName}`, - range: 'range' in reference.value ? reference.value.range : undefined, - }, { icon }); - } else { - const variable = this.chatVariablesService.getVariable(reference.variableName); - templateData.label.setLabel(`#${reference.variableName}`, undefined, { title: variable?.description }); - } - } else { - const uri = 'uri' in reference ? reference.uri : reference; - if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) { - templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: icon ?? Codicon.globe }); - } else { - templateData.label.setFile(uri, { - fileKind: FileKind.FILE, - // Should not have this live-updating data on a historical reference - fileDecorations: { badges: false, colors: false }, - range: 'range' in reference ? reference.range : undefined - }); - } - } - } - - disposeTemplate(templateData: IChatContentReferenceListTemplate): void { - templateData.templateDisposables.dispose(); - } -} - -class ResourcePool extends Disposable { - private readonly pool: T[] = []; - - private _inUse = new Set; - public get inUse(): ReadonlySet { - return this._inUse; - } - - constructor( - private readonly _itemFactory: () => T, - ) { - super(); - } - - get(): T { - if (this.pool.length > 0) { - const item = this.pool.pop()!; - this._inUse.add(item); - return item; - } - - const item = this._register(this._itemFactory()); - this._inUse.add(item); - return item; - } - - release(item: T): void { - this._inUse.delete(item); - this.pool.push(item); - } -} - class ChatVoteButton extends MenuEntryActionViewItem { override render(container: HTMLElement): void { super.render(container); container.classList.toggle('checked', this.action.checked); } } - -class ChatListTreeDelegate implements IListVirtualDelegate { - static readonly ITEM_HEIGHT = 22; - - getHeight(element: IChatResponseProgressFileTreeData): number { - return ChatListTreeDelegate.ITEM_HEIGHT; - } - - getTemplateId(element: IChatResponseProgressFileTreeData): string { - return 'chatListTreeTemplate'; - } -} - -class ChatListTreeCompressionDelegate implements ITreeCompressionDelegate { - isIncompressible(element: IChatResponseProgressFileTreeData): boolean { - return !element.children; - } -} - -interface IChatListTreeRendererTemplate { - templateDisposables: DisposableStore; - label: IResourceLabel; -} - -class ChatListTreeRenderer implements ICompressibleTreeRenderer { - templateId: string = 'chatListTreeTemplate'; - - constructor(private labels: ResourceLabels, private decorations: IFilesConfiguration['explorer']['decorations']) { } - - renderCompressedElements(element: ITreeNode, void>, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { - templateData.label.element.style.display = 'flex'; - const label = element.element.elements.map((e) => e.label); - templateData.label.setResource({ resource: element.element.elements[0].uri, name: label }, { - title: element.element.elements[0].label, - fileKind: element.children ? FileKind.FOLDER : FileKind.FILE, - extraClasses: ['explorer-item'], - fileDecorations: this.decorations - }); - } - renderTemplate(container: HTMLElement): IChatListTreeRendererTemplate { - const templateDisposables = new DisposableStore(); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); - return { templateDisposables, label }; - } - renderElement(element: ITreeNode, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { - templateData.label.element.style.display = 'flex'; - if (!element.children.length && element.element.type !== FileType.Directory) { - templateData.label.setFile(element.element.uri, { - fileKind: FileKind.FILE, - hidePath: true, - fileDecorations: this.decorations, - }); - } else { - templateData.label.setResource({ resource: element.element.uri, name: element.element.label }, { - title: element.element.label, - fileKind: FileKind.FOLDER, - fileDecorations: this.decorations - }); - } - } - disposeTemplate(templateData: IChatListTreeRendererTemplate): void { - templateData.templateDisposables.dispose(); - } -} - -class ChatListTreeDataSource implements IAsyncDataSource { - hasChildren(element: IChatResponseProgressFileTreeData): boolean { - return !!element.children; - } - - async getChildren(element: IChatResponseProgressFileTreeData): Promise> { - return element.children ?? []; - } -} - -function isInteractiveProgressTreeData(item: Object): item is IChatResponseProgressFileTreeData { - return 'label' in item; -} - -function contentToMarkdown(str: string | IMarkdownString): IMarkdownString { - return typeof str === 'string' ? { value: str } : str; -} - -function isProgressMessage(item: any): item is IChatProgressMessage { - return item && 'kind' in item && item.kind === 'progressMessage'; -} - -function isProgressTaskRenderData(item: any): item is IChatTaskRenderData { - return item && 'isSettled' in item; -} - -function isWarningRenderData(item: any): item is IChatWarningMessage { - return item && 'kind' in item && item.kind === 'warning'; -} - -function isProgressMessageRenderData(item: IChatRenderData): item is IChatProgressMessageRenderData { - return item && 'isAtEndOfResponse' in item; -} - -function isCommandButtonRenderData(item: IChatRenderData): item is IChatCommandButton { - return item && 'kind' in item && item.kind === 'command'; -} - -function isTextEditRenderData(item: IChatRenderData): item is IChatTextEditGroup { - return item && 'kind' in item && item.kind === 'textEditGroup'; -} - -function isConfirmationRenderData(item: IChatRenderData): item is IChatConfirmation { - return item && 'kind' in item && item.kind === 'confirmation'; -} - -function isMarkdownRenderData(item: IChatRenderData): item is IChatResponseMarkdownRenderData { - return item && 'renderedWordCount' in item; -} - -function onlyProgressMessagesAfterI(items: ReadonlyArray, i: number): boolean { - return items.slice(i).every(isProgressMessage); -} - -function onlyProgressMessageRenderDatasAfterI(items: ReadonlyArray, i: number): boolean { - return items.slice(i).every(isProgressMessageRenderData); -} diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 25c4ac91e44..9af542462c3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -42,7 +42,7 @@ export function agentToMarkdown(agent: IChatAgentData, isClickable: boolean, acc const isAllowed = chatAgentNameService.getAgentNameRestriction(agent); let name = `${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; - const isDupe = isAllowed && chatAgentService.getAgentsByName(agent.name).length > 1; + const isDupe = isAllowed && chatAgentService.agentHasDupeName(agent.id); if (isDupe) { name += ` (${agent.publisherDisplayName})`; } @@ -177,7 +177,7 @@ export class ChatMarkdownDecorationsRenderer { const agent = this.chatAgentService.getAgent(args.agentId); const hover: Lazy = new Lazy(() => store.add(this.instantiationService.createInstance(ChatAgentHover))); - store.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), container, () => { + store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), container, () => { hover.value.setAgent(args.agentId); return hover.value.domNode; }, agent && getChatAgentHoverOptions(() => agent, this.commandService))); diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts index 225ecdd120e..4f08d298d9c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts @@ -29,6 +29,8 @@ const allowedHtmlTags = [ 'p', 'pre', 'strong', + 'sub', + 'sup', 'table', 'tbody', 'td', @@ -39,8 +41,10 @@ const allowedHtmlTags = [ 'a', 'img', + // TODO@roblourens when we sanitize attributes in markdown source, we can ban these elements at that step. microsoft/vscode-copilot#5091 // Not in the official list, but used for codicons and other vscode markdown extensions 'span', + 'div', ]; /** @@ -71,7 +75,8 @@ export class ChatMarkdownRenderer extends MarkdownRenderer { ...markdown, // dompurify uses DOMParser, which strips leading comments. Wrapping it all in 'body' prevents this. - value: `${markdown.value}`, + // The \n\n prevents marked.js from parsing the body contents as just text in an 'html' token, instead of actual markdown. + value: `\n\n${markdown.value}`, } : markdown; return super.render(mdWithBody, options, markedOptions); diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index c89a6a4d712..a1c6d7a732d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -23,6 +23,9 @@ import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workben import { IRawChatParticipantContribution } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { Action } from 'vs/base/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatParticipants', @@ -40,7 +43,7 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi type: 'string' }, name: { - description: localize('chatParticipantName', "User-facing name for this chat participant. The user will use '@' with this name to invoke the participant."), + description: localize('chatParticipantName', "User-facing name for this chat participant. The user will use '@' with this name to invoke the participant. Name must not contain whitespace."), type: 'string', pattern: '^[\\w0-9_-]+$' }, @@ -60,6 +63,10 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi description: localize('chatSampleRequest', "When the user clicks this participant in `/help`, this text will be submitted to the participant."), type: 'string' }, + when: { + description: localize('chatParticipantWhen', "A condition which must be true to enable this participant."), + type: 'string' + }, commands: { markdownDescription: localize('chatCommandsDescription', "Commands available for this chat participant, which the user can invoke with a `/`."), type: 'array', @@ -116,6 +123,8 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { @IProductService private readonly productService: IProductService, @IContextKeyService private readonly contextService: IContextKeyService, @ILogService private readonly logService: ILogService, + @INotificationService private readonly notificationService: INotificationService, + @ICommandService private readonly commandService: ICommandService, ) { this._viewContainer = this.registerViewContainer(); this.registerListeners(); @@ -159,8 +168,20 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { private handleAndRegisterChatExtensions(): void { chatParticipantExtensionPoint.setHandler((extensions, delta) => { for (const extension of delta.added) { - if (this.productService.quality === 'stable' && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { - this.logService.warn(`Chat participants are not yet enabled in VS Code Stable (${extension.description.identifier.value})`); + // Detect old version of Copilot Chat extension. + // TODO@roblourens remove after one release, after this we will rely on things like the API version + if (extension.value.some(participant => participant.id === 'github.copilot.default' && !participant.fullName)) { + this.notificationService.notify({ + severity: Severity.Error, + message: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date."), + actions: { + primary: [ + new Action('showExtension', localize('action.showExtension', "Show Extension"), undefined, true, () => { + return this.commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', ['GitHub.copilot-chat']); + }) + ] + } + }); continue; } @@ -210,6 +231,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { extensionDisplayName: extension.description.displayName ?? extension.description.name, id: providerDescriptor.id, description: providerDescriptor.description, + when: providerDescriptor.when, metadata: { isSticky: providerDescriptor.isSticky, sampleRequest: providerDescriptor.sampleRequest, @@ -217,7 +239,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { name: providerDescriptor.name, fullName: providerDescriptor.fullName, isDefault: providerDescriptor.isDefault, - defaultImplicitVariables: providerDescriptor.defaultImplicitVariables, locations: isNonEmptyArray(providerDescriptor.locations) ? providerDescriptor.locations.map(ChatAgentLocation.fromRaw) : [ChatAgentLocation.Panel], diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index d97ecba4f42..6d682b5d72e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -47,7 +47,7 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplentation { widget.focus(focusedItem); const isWelcome = focusedItem instanceof ChatWelcomeMessageModel; - let responseContent = isResponseVM(focusedItem) ? focusedItem.response.asString() : undefined; + let responseContent = isResponseVM(focusedItem) ? focusedItem.response.toString() : undefined; if (isWelcome) { const welcomeReplyContents = []; for (const content of focusedItem.content) { @@ -95,4 +95,5 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplentation { }; } } + dispose() { } } diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index acde7f02757..cd978afa041 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -11,7 +11,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Location } from 'vs/editor/common/languages'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatWidgetService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatDynamicVariableModel } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; @@ -19,6 +19,7 @@ import { ChatRequestDynamicVariablePart, ChatRequestVariablePart, IParsedChatReq import { IChatContentReference } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ChatContextAttachments } from 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; interface IChatData { data: IChatVariableData; @@ -31,7 +32,8 @@ export class ChatVariablesService implements IChatVariablesService { private _resolver = new Map(); constructor( - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IViewsService private readonly viewsService: IViewsService, ) { } @@ -88,11 +90,15 @@ export class ChatVariablesService implements IChatVariablesService { await Promise.allSettled(jobs); + // Make array not sparse resolvedVariables = coalesce(resolvedVariables); // "reverse", high index first so that replacement is simple resolvedVariables.sort((a, b) => b.range!.start - a.range!.start); - resolvedVariables.push(...resolvedAttachedContext); + + // resolvedAttachedContext is a sparse array + resolvedVariables.push(...coalesce(resolvedAttachedContext)); + return { variables: resolvedVariables, @@ -155,6 +161,7 @@ export class ChatVariablesService implements IChatVariablesService { return; } + await showChatView(this.viewsService); const widget = this.chatWidgetService.lastFocusedWidget; if (!widget || !widget.viewModel) { return; @@ -164,7 +171,7 @@ export class ChatVariablesService implements IChatVariablesService { if (key === 'file' && typeof value !== 'string') { const uri = URI.isUri(value) ? value : value.uri; const range = 'range' in value ? value.range : undefined; - widget.getContrib(ChatContextAttachments.ID)?.setContext(false, { value, id: uri.toString() + (range?.toString() ?? ''), name: basename(uri.path), isDynamic: true }); + widget.getContrib(ChatContextAttachments.ID)?.setContext(false, { value, id: uri.toString() + (range?.toString() ?? ''), name: basename(uri.path), isFile: true, isDynamic: true }); return; } diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index b0c54404399..80022029e40 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -22,7 +22,6 @@ import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/vie import { Memento } from 'vs/workbench/common/memento'; import { SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { IChatViewPane } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatViewState, ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CHAT_PROVIDER_ID } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; @@ -35,7 +34,7 @@ interface IViewPaneState extends IChatViewState { } export const CHAT_SIDEBAR_PANEL_ID = 'workbench.panel.chatSidebar'; -export class ChatViewPane extends ViewPane implements IChatViewPane { +export class ChatViewPane extends ViewPane { private _widget!: ChatWidget; get widget(): ChatWidget { return this._widget; } @@ -134,7 +133,7 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { try { super.renderBody(parent); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); const locationBasedColors = this.getLocationBasedColors(); this._widget = this._register(scopedInstantiationService.createInstance( ChatWidget, @@ -176,7 +175,7 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { this._widget.acceptInput(query); } - clear(): void { + private clear(): void { if (this.widget.viewModel) { this.chatService.clearSession(this.widget.viewModel.sessionId); } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 6b19f886cf1..21ad0513b63 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -9,8 +9,8 @@ import { disposableTimeout, timeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import { isEqual } from 'vs/base/common/resources'; +import { matchesScheme, Schemas } from 'vs/base/common/network'; +import { extUri, isEqual } from 'vs/base/common/resources'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/chat'; @@ -32,11 +32,11 @@ import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { ChatListDelegate, ChatListItemRenderer, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_QUICK_CHAT, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatModelInitState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { IChatFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatLocationData, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; @@ -69,12 +69,19 @@ export interface IChatWidgetContrib extends IDisposable { */ getInputState?(): any; + onDidChangeInputState?: Event; + /** * Called with the result of getInputState when navigating input history. */ setInputState?(s: any): void; } +export interface IChatWidgetLocationOptions { + location: ChatAgentLocation; + resolveData?(): IChatLocationData | undefined; +} + export class ChatWidget extends Disposable implements IChatWidget { public static readonly CONTRIBS: { new(...args: [IChatWidget, ...any]): IChatWidgetContrib }[] = []; @@ -105,13 +112,16 @@ export class ChatWidget extends Disposable implements IChatWidget { private _onDidChangeParsedInput = this._register(new Emitter()); readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event; + private readonly _onWillMaybeChangeHeight = new Emitter(); + readonly onWillMaybeChangeHeight: Event = this._onWillMaybeChangeHeight.event; + private _onDidChangeHeight = this._register(new Emitter()); readonly onDidChangeHeight = this._onDidChangeHeight.event; private readonly _onDidChangeContentHeight = new Emitter(); readonly onDidChangeContentHeight: Event = this._onDidChangeContentHeight.event; - private contribs: IChatWidgetContrib[] = []; + private contribs: ReadonlyArray = []; private tree!: WorkbenchObjectTree; private renderer!: ChatListItemRenderer; @@ -171,8 +181,14 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.contextKeyService; } + private readonly _location: IChatWidgetLocationOptions; + + get location() { + return this._location.location; + } + constructor( - readonly location: ChatAgentLocation, + location: ChatAgentLocation | IChatWidgetLocationOptions, readonly viewContext: IChatWidgetViewContext, private readonly viewOptions: IChatWidgetViewOptions, private readonly styles: IChatWidgetStyles, @@ -189,8 +205,16 @@ export class ChatWidget extends Disposable implements IChatWidget { @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, ) { super(); + + if (typeof location === 'object') { + this._location = location; + } else { + this._location = { location }; + } + CONTEXT_IN_CHAT_SESSION.bindTo(contextKeyService).set(true); - CONTEXT_CHAT_LOCATION.bindTo(contextKeyService).set(location); + CONTEXT_CHAT_LOCATION.bindTo(contextKeyService).set(this._location.location); + CONTEXT_IN_QUICK_CHAT.bindTo(contextKeyService).set('resource' in viewContext); this.agentInInput = CONTEXT_CHAT_INPUT_HAS_AGENT.bindTo(contextKeyService); this.requestInProgress = CONTEXT_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); @@ -199,11 +223,18 @@ export class ChatWidget extends Disposable implements IChatWidget { this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection)); this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise => { - if (input.resource.scheme !== Schemas.vscodeChatCodeBlock) { + let resource = input.resource; + + // if trying to open backing documents, actually open the real chat code block doc + if (matchesScheme(resource, Schemas.vscodeCopilotBackingChatCodeBlock)) { + resource = resource.with({ scheme: Schemas.vscodeChatCodeBlock }); + } + + if (resource.scheme !== Schemas.vscodeChatCodeBlock) { return null; } - const responseId = input.resource.path.split('/').at(1); + const responseId = resource.path.split('/').at(1); if (!responseId) { return null; } @@ -213,12 +244,14 @@ export class ChatWidget extends Disposable implements IChatWidget { return null; } + // TODO: needs to reveal the chat view + this.reveal(item); await timeout(0); // wait for list to actually render for (const editor of this.renderer.editorsInUse() ?? []) { - if (editor.uri?.toString() === input.resource.toString()) { + if (extUri.isEqual(editor.uri, resource, true)) { const inner = editor.editor; if (input.options?.selection) { inner.setSelection({ @@ -301,6 +334,15 @@ export class ChatWidget extends Disposable implements IChatWidget { return undefined; } }).filter(isDefined); + + this.contribs.forEach(c => { + if (c.onDidChangeInputState) { + this._register(c.onDidChangeInputState(() => { + const state = this.collectInputState(); + this.inputPart.updateState(state); + })); + } + }); } getContrib(id: string): T | undefined { @@ -353,6 +395,8 @@ export class ChatWidget extends Disposable implements IChatWidget { }; }); + this._onWillMaybeChangeHeight.fire(); + this.tree.setChildren(null, treeItems, { diffIdentityProvider: { getId: (element) => { @@ -414,7 +458,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void { - const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]))); + const scopedInstantiationService = this._register(this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])))); const delegate = scopedInstantiationService.createInstance(ChatListDelegate, this.viewOptions.defaultElementHeight ?? 200); const rendererDelegate: IChatRendererDelegate = { getListLength: () => this.tree.getNode(null).visibleChildrenCount, @@ -455,6 +499,7 @@ export class ChatWidget extends Disposable implements IChatWidget { { identityProvider: { getId: (e: ChatTreeItem) => e.id }, horizontalScrolling: false, + alwaysConsumeMouseWheel: false, supportDynamicHeights: true, hideTwistiesOfChildlessElements: true, accessibilityProvider: this.instantiationService.createInstance(ChatAccessibilityProvider), @@ -527,12 +572,12 @@ export class ChatWidget extends Disposable implements IChatWidget { this._onDidChangeContentHeight.fire(); } - private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'default' | 'compact' }): void { + private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'default' | 'compact' | 'minimal' }): void { this.inputPart = this._register(this.instantiationService.createInstance(ChatInputPart, this.location, { renderFollowups: options?.renderFollowups ?? true, - renderStyle: options?.renderStyle, + renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle, menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }, editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, } @@ -541,8 +586,9 @@ export class ChatWidget extends Disposable implements IChatWidget { this._register(this.inputPart.onDidLoadInputState(state => { this.contribs.forEach(c => { - if (c.setInputState && typeof state === 'object' && state?.[c.id]) { - c.setInputState(state[c.id]); + if (c.setInputState) { + const contribState = (typeof state === 'object' && state?.[c.id]) ?? {}; + c.setInputState(contribState); } }); })); @@ -582,6 +628,7 @@ export class ChatWidget extends Disposable implements IChatWidget { sessionId: this.viewModel.sessionId, requestId: e.response.requestId, agentId: e.response.agent?.id, + command: e.response.slashCommand?.name, result: e.response.result, action: { kind: 'followUp', @@ -635,7 +682,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.viewModel = undefined; this.onDidChangeItems(); })); - this.inputPart.setState(viewState.inputValue); + this.inputPart.initForNewChatModel(viewState.inputValue, viewState.inputState ?? this.collectInputState()); this.contribs.forEach(c => { if (c.setInputState && viewState.inputState?.[c.id]) { c.setInputState(viewState.inputState?.[c.id]); @@ -681,13 +728,17 @@ export class ChatWidget extends Disposable implements IChatWidget { } setInput(value = ''): void { - this.inputPart.setValue(value); + this.inputPart.setValue(value, false); } getInput(): string { return this.inputPart.inputEditor.getValue(); } + logInputHistory(): void { + this.inputPart.logInputHistory(); + } + async acceptInput(query?: string): Promise { return this._acceptInput(query ? { query } : undefined); } @@ -716,13 +767,18 @@ export class ChatWidget extends Disposable implements IChatWidget { 'query' in opts ? opts.query : `${opts.prefix} ${editorValue}`; const isUserQuery = !opts || 'prefix' in opts; - const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { location: this.location, parserContext: { selectedAgent: this._lastSelectedAgent }, attachedContext: [...this.inputPart.attachedContext.values()] }); + const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { + location: this.location, + locationData: this._location.resolveData?.(), + parserContext: { selectedAgent: this._lastSelectedAgent }, + attachedContext: [...this.inputPart.attachedContext.values()] + }); if (result) { this.inputPart.attachedContext.clear(); - const inputState = this.collectInputState(); - this.inputPart.acceptInput(isUserQuery ? input : undefined, isUserQuery ? inputState : undefined); + this.inputPart.acceptInput(isUserQuery); this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand }); + this.inputPart.updateState(this.collectInputState()); result.responseCompletePromise.then(() => { const responses = this.viewModel?.getItems().filter(isResponseVM); const lastResponse = responses?.[responses.length - 1]; @@ -907,8 +963,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.inputPart.saveState(); return { inputValue: this.getInput(), inputState: this.collectInputState() }; } - - } export class ChatWidgetService implements IChatWidgetService { diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.css b/src/vs/workbench/contrib/chat/browser/codeBlockPart.css index 50c903e62e5..89596f89748 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.css +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.css @@ -8,15 +8,30 @@ position: relative; } -.interactive-result-code-block .monaco-toolbar { +.interactive-result-code-block .interactive-result-code-block-toolbar { display: none; +} + +.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-action-bar, +.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-toolbar { position: absolute; top: -13px; - right: 10px; height: 26px; + line-height: 26px; background-color: var(--vscode-interactive-result-editor-background-color, var(--vscode-editor-background)); border: 1px solid var(--vscode-chat-requestBorder); z-index: 100; + max-width: 70%; + text-overflow: ellipsis; + overflow: hidden; +} + +.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-action-bar { + left: 0px +} + +.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-toolbar { + right: 10px; } .interactive-result-code-block .monaco-toolbar .action-item { @@ -29,9 +44,9 @@ margin: 1px; } -.interactive-result-code-block:hover .monaco-toolbar, -.interactive-result-code-block .monaco-toolbar:focus-within, -.interactive-result-code-block.focused .monaco-toolbar { +.interactive-result-code-block:hover .interactive-result-code-block-toolbar, +.interactive-result-code-block .interactive-result-code-block-toolbar:focus-within, +.interactive-result-code-block.focused .interactive-result-code-block-toolbar { display: initial; border-radius: 2px; } @@ -128,3 +143,21 @@ color: var(--vscode-textLink-foreground); cursor: pointer; } + +.interactive-result-code-block.compare .message A > CODE { + color: var(--vscode-textLink-foreground); +} + +.interactive-result-code-block.compare .interactive-result-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 3px; + box-sizing: border-box; + border-bottom: solid 1px var(--vscode-chat-requestBorder); +} + +.interactive-result-code-block.compare.no-diff .interactive-result-header, +.interactive-result-code-block.compare.no-diff .interactive-result-editor { + display: none; +} diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index e4dfff5f9e7..7caa40d9684 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -6,24 +6,37 @@ import 'vs/css!./codeBlockPart'; import * as dom from 'vs/base/browser/dom'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { Button } from 'vs/base/browser/ui/button/button'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; +import { TabFocus } from 'vs/editor/browser/config/tabFocus'; +import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { EDITOR_FONT_DEFAULTS, EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; import { IDiffEditorViewModel, ScrollType } from 'vs/editor/common/editorCommon'; +import { TextEdit } from 'vs/editor/common/languages'; import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model'; +import { TextModelText } from 'vs/editor/common/model/textModelText'; import { IModelService } from 'vs/editor/common/services/model'; +import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { BracketMatchingController } from 'vs/editor/contrib/bracketMatching/browser/bracketMatching'; +import { ColorDetector } from 'vs/editor/contrib/colorPicker/browser/colorDetector'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import { ViewportSemanticTokensContribution } from 'vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens'; import { SmartSelectController } from 'vs/editor/contrib/smartSelect/browser/smartSelect'; import { WordHighlighterContribution } from 'vs/editor/contrib/wordHighlighter/browser/wordHighlighter'; @@ -33,34 +46,24 @@ import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; +import { CONTEXT_CHAT_EDIT_APPLIED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatResponseModel, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IMarkdownVulnerability } from '../common/annotations'; -import { TabFocus } from 'vs/editor/browser/config/tabFocus'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; -import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; -import { CONTEXT_CHAT_EDIT_APPLIED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IChatResponseModel, IChatTextEditGroup } from 'vs/workbench/contrib/chat/common/chatModel'; -import { TextEdit } from 'vs/editor/common/languages'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { isEqual } from 'vs/base/common/resources'; -import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { TextModelText } from 'vs/editor/common/model/textModelText'; -import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; +import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { FileKind } from 'vs/platform/files/common/files'; const $ = dom.$; @@ -141,6 +144,7 @@ export class CodeBlockPart extends Disposable { private currentScrollWidth = 0; private readonly disposableStore = this._register(new DisposableStore()); + private isDisposed = false; constructor( private readonly options: ChatEditorOptions, @@ -157,7 +161,7 @@ export class CodeBlockPart extends Disposable { this.element = $('.interactive-result-code-block'); this.contextKeyService = this._register(contextKeyService.createScoped(this.element)); - const scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])); + const scopedInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]))); const editorElement = dom.append(this.element, $('.interactive-result-editor')); this.editor = this.createEditor(scopedInstantiationService, editorElement, { ...getSimpleEditorOptions(this.configurationService), @@ -187,7 +191,7 @@ export class CodeBlockPart extends Disposable { const toolbarElement = dom.append(this.element, $('.interactive-result-code-block-toolbar')); const editorScopedService = this.editor.contextKeyService.createScoped(toolbarElement); - const editorScopedInstantiationService = scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService])); + const editorScopedInstantiationService = this._register(scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService]))); this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarElement, menuId, { menuOptions: { shouldForwardArgs: true @@ -261,6 +265,11 @@ export class CodeBlockPart extends Disposable { } } + override dispose() { + this.isDisposed = true; + super.dispose(); + } + get uri(): URI | undefined { return this.editor.getModel()?.uri; } @@ -280,6 +289,7 @@ export class CodeBlockPart extends Disposable { HoverController.ID, MessageController.ID, GotoDefinitionAtPositionEditorContribution.ID, + ColorDetector.ID ]) })); } @@ -352,6 +362,9 @@ export class CodeBlockPart extends Disposable { } await this.updateEditor(data); + if (this.isDisposed) { + return; + } this.layout(width); if (editable) { @@ -456,7 +469,7 @@ export interface ICodeCompareBlockData { readonly diffData: Promise; readonly parentContextKeyService?: IContextKeyService; - readonly hideToolbar?: boolean; + // readonly hideToolbar?: boolean; } @@ -466,6 +479,7 @@ export class CodeCompareBlockPart extends Disposable { private readonly contextKeyService: IContextKeyService; private readonly diffEditor: DiffEditorWidget; + private readonly resourceLabel: ResourceLabel; private readonly toolbar: MenuWorkbenchToolBar; readonly element: HTMLElement; private readonly messageElement: HTMLElement; @@ -495,7 +509,8 @@ export class CodeCompareBlockPart extends Disposable { this.messageElement.tabIndex = 0; this.contextKeyService = this._register(contextKeyService.createScoped(this.element)); - const scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])); + const scopedInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]))); + const editorHeader = dom.append(this.element, $('.interactive-result-header.show-file-icons')); const editorElement = dom.append(this.element, $('.interactive-result-editor')); this.diffEditor = this.createDiffEditor(scopedInstantiationService, editorElement, { ...getSimpleEditorOptions(this.configurationService), @@ -522,20 +537,16 @@ export class CodeCompareBlockPart extends Disposable { ...this.getEditorOptionsFromConfig(), }); - const toolbarElement = dom.append(this.element, $('.interactive-result-code-block-toolbar')); - const editorScopedService = this.diffEditor.getModifiedEditor().contextKeyService.createScoped(toolbarElement); - const editorScopedInstantiationService = scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService])); - this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarElement, menuId, { + this.resourceLabel = this._register(scopedInstantiationService.createInstance(ResourceLabel, editorHeader, { supportIcons: true })); + + const editorScopedService = this.diffEditor.getModifiedEditor().contextKeyService.createScoped(editorHeader); + const editorScopedInstantiationService = this._register(scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService]))); + this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, editorHeader, menuId, { menuOptions: { shouldForwardArgs: true } })); - - this._register(this.toolbar.onDidChangeDropdownVisibility(e => { - toolbarElement.classList.toggle('force-visibility', e); - })); - this._configureForScreenReader(); this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this._configureForScreenReader())); this._register(this.configurationService.onDidChangeConfiguration((e) => { @@ -609,6 +620,7 @@ export class CodeCompareBlockPart extends Disposable { isInEmbeddedEditor: true, useInlineViewWhenSpaceIsLimited: false, hideUnchangedRegions: { enabled: true, contextLineCount: 1 }, + renderGutterMenu: false, ...options }, { originalEditor: widgetOptions, modifiedEditor: widgetOptions })); } @@ -682,11 +694,10 @@ export class CodeCompareBlockPart extends Disposable { this.layout(width); this.diffEditor.updateOptions({ ariaLabel: localize('chat.compareCodeBlockLabel', "Code Edits") }); - if (data.hideToolbar) { - dom.hide(this.toolbar.getElement()); - } else { - dom.show(this.toolbar.getElement()); - } + this.resourceLabel.element.setFile(data.edit.uri, { + fileKind: FileKind.FILE, + fileDecorations: { colors: true, badges: false } + }); } reset() { @@ -714,12 +725,17 @@ export class CodeCompareBlockPart extends Disposable { const uriLabel = this.labelService.getUriLabel(data.edit.uri, { relative: true, noPrefix: true }); - const template = data.edit.state.applied > 1 - ? localize('chat.edits.N', "Made {0} changes in [[{1}]]", data.edit.state.applied, uriLabel) - : localize('chat.edits.1', "Made 1 change in [[{0}]]", uriLabel); - + let template: string; + if (data.edit.state.applied === 1) { + template = localize('chat.edits.1', "Made 1 change in [[``{0}``]]", uriLabel); + } else if (data.edit.state.applied < 0) { + template = localize('chat.edits.rejected', "Edits in [[``{0}``]] have been rejected", uriLabel); + } else { + template = localize('chat.edits.N', "Made {0} changes in [[``{1}``]]", data.edit.state.applied, uriLabel); + } const message = renderFormattedText(template, { + renderCodeSegments: true, actionHandler: { callback: () => { this.openerService.open(data.edit.uri, { fromUserGesture: true, allowCommands: false }); @@ -775,7 +791,7 @@ export class DefaultChatTextEditor { @IDialogService private readonly dialogService: IDialogService, ) { } - async apply(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup): Promise { + async apply(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup, diffEditor: IDiffEditor | undefined): Promise { if (!response.response.value.includes(item)) { // bogous item @@ -787,15 +803,16 @@ export class DefaultChatTextEditor { return; } - let diffEditor: IDiffEditor | undefined; - for (const candidate of this.editorService.listDiffEditors()) { - if (!candidate.getContainerDomNode().isConnected) { - continue; - } - const model = candidate.getModel(); - if (!model || !isEqual(model.original.uri, item.uri) || model.modified.uri.scheme !== Schemas.vscodeChatCodeCompareBlock) { - diffEditor = candidate; - break; + if (!diffEditor) { + for (const candidate of this.editorService.listDiffEditors()) { + if (!candidate.getContainerDomNode().isConnected) { + continue; + } + const model = candidate.getModel(); + if (!model || !isEqual(model.original.uri, item.uri) || model.modified.uri.scheme !== Schemas.vscodeChatCodeCompareBlock) { + diffEditor = candidate; + break; + } } } @@ -868,4 +885,18 @@ export class DefaultChatTextEditor { } return true; } + + discard(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup) { + if (!response.response.value.includes(item)) { + // bogous item + return; + } + + if (item.state?.applied) { + // already applied + return; + } + + response.setEditApplied(item, -1); + } } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts index ff0a4455486..c15a1f6aee3 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatWidget, IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; @@ -12,6 +13,9 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon private _attachedContext = new Set(); + private readonly _onDidChangeInputState = this._register(new Emitter()); + readonly onDidChangeInputState = this._onDidChangeInputState.event; + public static readonly ID = 'chatContextAttachments'; get id() { @@ -30,13 +34,18 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon })); } - getInputState?() { + getInputState(): IChatRequestVariableEntry[] { return [...this._attachedContext.values()]; } - setInputState?(s: any): void { + setInputState(s: any): void { if (!Array.isArray(s)) { - return; + s = []; + } + + this._attachedContext.clear(); + for (const attachment of s) { + this._attachedContext.add(attachment); } this.widget.setContext(true, ...s); @@ -55,10 +64,12 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon } this.widget.setContext(overwrite, ...attachments); + this._onDidChangeInputState.fire(); } private _removeContext(attachment: IChatRequestVariableEntry) { this._attachedContext.delete(attachment); + this._onDidChangeInputState.fire(); } private _clearAttachedContext() { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 4c3fa0ee29c..2d8bf253fd0 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce } from 'vs/base/common/arrays'; +import { Emitter } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/resources'; @@ -38,24 +39,30 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC return ChatDynamicVariableModel.ID; } + private _onDidChangeInputState = this._register(new Emitter()); + readonly onDidChangeInputState = this._onDidChangeInputState.event; + constructor( private readonly widget: IChatWidget, @ILabelService private readonly labelService: ILabelService, - @ILogService private readonly logService: ILogService, ) { super(); this._register(widget.inputEditor.onDidChangeModelContent(e => { e.changes.forEach(c => { // Don't mutate entries in _variables, since they will be returned from the getter + const originalNumVariables = this._variables.length; this._variables = coalesce(this._variables.map(ref => { const intersection = Range.intersectRanges(ref.range, c.range); if (intersection && !intersection.isEmpty()) { - // The reference text was changed, it's broken - const rangeToDelete = new Range(ref.range.startLineNumber, ref.range.startColumn, ref.range.endLineNumber, ref.range.endColumn - 1); - this.widget.inputEditor.executeEdits(this.id, [{ - range: rangeToDelete, - text: '', - }]); + // The reference text was changed, it's broken. + // But if the whole reference range was deleted (eg history navigation) then don't try to change the editor. + if (!Range.containsRange(c.range, ref.range)) { + const rangeToDelete = new Range(ref.range.startLineNumber, ref.range.startColumn, ref.range.endLineNumber, ref.range.endColumn - 1); + this.widget.inputEditor.executeEdits(this.id, [{ + range: rangeToDelete, + text: '', + }]); + } return null; } else if (Range.compareRangesUsingStarts(ref.range, c.range) > 0) { const delta = c.text.length - c.rangeLength; @@ -72,6 +79,10 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC return ref; })); + + if (this._variables.length !== originalNumVariables) { + this._onDidChangeInputState.fire(); + } }); this.updateDecorations(); @@ -84,9 +95,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC setInputState(s: any): void { if (!Array.isArray(s)) { - // Something went wrong - this.logService.warn('ChatDynamicVariableModel.setInputState called with invalid state: ' + JSON.stringify(s)); - return; + s = []; } this._variables = s; @@ -96,6 +105,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC addReference(ref: IDynamicVariable): void { this._variables.push(ref); this.updateDecorations(); + this._onDidChangeInputState.fire(); } private updateDecorations(): void { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index c94042618c9..8910e307eab 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -13,6 +13,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; @@ -39,7 +40,7 @@ class SlashCommandCompletions extends Disposable { triggerCharacters: ['/'], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || (widget.location !== ChatAgentLocation.Panel && widget.location !== ChatAgentLocation.Notebook) /* TODO@jrieken - enable when agents are adopted*/) { + if (!widget || !widget.viewModel) { return null; } @@ -55,7 +56,7 @@ class SlashCommandCompletions extends Disposable { return; } - const slashCommands = this.chatSlashCommandService.getCommands(); + const slashCommands = this.chatSlashCommandService.getCommands(widget.location); if (!slashCommands) { return null; } @@ -95,7 +96,7 @@ class AgentCompletions extends Disposable { triggerCharacters: ['@'], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || (widget.location !== ChatAgentLocation.Panel && widget.location !== ChatAgentLocation.Notebook) /* TODO@jrieken - enable when agents are adopted*/) { + if (!widget || !widget.viewModel) { return null; } @@ -117,7 +118,7 @@ class AgentCompletions extends Disposable { return { suggestions: agents.map((agent, i): CompletionItem => { - const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); return { // Leading space is important because detail has no space at the start by design label: isDupe ? @@ -139,7 +140,7 @@ class AgentCompletions extends Disposable { triggerCharacters: ['/'], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + if (!widget || !widget.viewModel) { return; } @@ -191,7 +192,7 @@ class AgentCompletions extends Disposable { provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); const viewModel = widget?.viewModel; - if (!widget || !viewModel || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + if (!widget || !viewModel) { return; } @@ -203,10 +204,20 @@ class AgentCompletions extends Disposable { const agents = this.chatAgentService.getAgents() .filter(a => a.locations.includes(widget.location)); + // When the input is only `/`, items are sorted by sortText. + // When typing, filterText is used to score and sort. + // The same list is refiltered/ranked while typing. + const getFilterText = (agent: IChatAgentData, command: string) => { + // This is hacking the filter algorithm to make @terminal /explain match worse than @workspace /explain by making its match index later in the string. + // When I type `/exp`, the workspace one should be sorted over the terminal one. + const dummyPrefix = agent.id === 'github.copilot.terminalPanel' ? `0000` : ``; + return `${chatSubcommandLeader}${dummyPrefix}${agent.name}.${command}`; + }; + const justAgents: CompletionItem[] = agents .filter(a => !a.isDefault) .map(agent => { - const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); const detail = agent.description; return { @@ -218,7 +229,7 @@ class AgentCompletions extends Disposable { insertText: `${agentLabel} `, range: new Range(1, 1, 1, 1), kind: CompletionItemKind.Text, - sortText: `${chatSubcommandLeader}${agent.id}`, + sortText: `${chatSubcommandLeader}${agent.name}`, command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, }; }); @@ -226,24 +237,40 @@ class AgentCompletions extends Disposable { return { suggestions: justAgents.concat( agents.flatMap(agent => agent.slashCommands.map((c, i) => { - const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); const withSlash = `${chatSubcommandLeader}${c.name}`; - return { + const item: CompletionItem = { label: { label: withSlash, description: agentLabel, detail: isDupe ? ` (${agent.publisherDisplayName})` : undefined }, - filterText: `${chatSubcommandLeader}${agent.name}${c.name}`, + filterText: getFilterText(agent, c.name), commitCharacters: [' '], insertText: `${agentLabel} ${withSlash} `, detail: `(${agentLabel}) ${c.description ?? ''}`, range: new Range(1, 1, 1, 1), kind: CompletionItemKind.Text, // The icons are disabled here anyway - sortText: `${chatSubcommandLeader}${agent.id}${c.name}`, + sortText: `${chatSubcommandLeader}${agent.name}${c.name}`, command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, - } satisfies CompletionItem; + }; + + if (agent.isDefault) { + // default agent isn't mentioned nor inserted + item.label = withSlash; + item.insertText = `${withSlash} `; + item.detail = c.description; + } + + return item; }))) }; } })); } + + private getAgentCompletionDetails(agent: IChatAgentData): { label: string; isDupe: boolean } { + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent); + const agentLabel = `${chatAgentLeader}${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; + const isDupe = isAllowed && this.chatAgentService.agentHasDupeName(agent.id); + return { label: agentLabel, isDupe }; + } } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); @@ -287,7 +314,7 @@ class BuiltinDynamicCompletions extends Disposable { triggerCharacters: [chatVariableLeader], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.supportsFileReferences || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + if (!widget || !widget.supportsFileReferences) { return null; } @@ -344,6 +371,7 @@ class VariableCompletions extends Disposable { @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + @IConfigurationService configService: IConfigurationService, ) { super(); @@ -352,8 +380,17 @@ class VariableCompletions extends Disposable { triggerCharacters: [chatVariableLeader], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const locations = new Set(); + locations.add(ChatAgentLocation.Panel); + + for (const value of Object.values(ChatAgentLocation)) { + if (typeof value === 'string' && configService.getValue(`chat.experimental.variables.${value}`)) { + locations.add(value); + } + } + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + if (!widget || !locations.has(widget.location)) { return null; } @@ -391,11 +428,3 @@ class VariableCompletions extends Disposable { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(VariableCompletions, LifecyclePhase.Eventually); - -function getAgentCompletionDetails(agent: IChatAgentData, otherAgents: IChatAgentData[], chatAgentNameService: IChatAgentNameService): { label: string; isDupe: boolean } { - const isAllowed = chatAgentNameService.getAgentNameRestriction(agent); - const agentLabel = `${chatAgentLeader}${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; - const isDupe = isAllowed && !!otherAgents.find(other => other.name === agent.name && other.id !== agent.id); - - return { label: agentLabel, isDupe }; -} diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index bafc22d77ee..24aafd6c101 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -14,7 +14,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { dynamicVariableDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; @@ -175,8 +175,8 @@ class InputEditorDecorations extends Disposable { } } - const onlyAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart); - if (onlyAgentCommandAndWhitespace) { + const onlyAgentAndAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart); + if (onlyAgentAndAgentCommandAndWhitespace) { // Agent reference and subcommand with no other text - show the placeholder const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, agentSubcommandPart.command.name)); const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentSubcommandPart.command.followupPlaceholder; @@ -193,17 +193,30 @@ class InputEditorDecorations extends Disposable { } } + const onlyAgentCommandAndWhitespace = agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentSubcommandPart); + if (onlyAgentCommandAndWhitespace) { + // Agent subcommand with no other text - show the placeholder + if (agentSubcommandPart?.command.description && exactlyOneSpaceAfterPart(agentSubcommandPart)) { + placeholderDecoration = [{ + range: getRangeForPlaceholder(agentSubcommandPart), + renderOptions: { + after: { + contentText: agentSubcommandPart.command.description, + color: this.getPlaceholderColor(), + } + } + }]; + } + } + this.widget.inputEditor.setDecorationsByType(decorationDescription, placeholderDecorationType, placeholderDecoration ?? []); const textDecorations: IDecorationOptions[] | undefined = []; if (agentPart) { - const isDupe = !!this.chatAgentService.getAgents().find(other => other.name === agentPart.agent.name && other.id !== agentPart.agent.id); - const publisher = isDupe ? `(${agentPart.agent.publisherDisplayName}) ` : ''; - const agentHover = `${publisher}${agentPart.agent.description}`; - textDecorations.push({ range: agentPart.editorRange, hoverMessage: new MarkdownString(agentHover) }); - if (agentSubcommandPart) { - textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); - } + textDecorations.push({ range: agentPart.editorRange }); + } + if (agentSubcommandPart) { + textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); } if (slashCommandPart) { @@ -278,7 +291,7 @@ class ChatTokenDeleter extends Disposable { // If this was a simple delete, try to find out whether it was inside a token if (!change.text && this.widget.viewModel) { - const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, ChatAgentLocation.Panel, { selectedAgent: previousSelectedAgent }); + const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, widget.location, { selectedAgent: previousSelectedAgent }); // For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestVariablePart); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts new file mode 100644 index 00000000000..893353b52f1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatEditorHoverWrapper } from 'vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper'; +import { IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { extractAgentAndCommand } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import * as nls from 'vs/nls'; + +export class ChatAgentHoverParticipant implements IEditorHoverParticipant { + + public readonly hoverOrdinal: number = 1; + + constructor( + private readonly editor: ICodeEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @ICommandService private readonly commandService: ICommandService, + ) { } + + public computeSync(anchor: HoverAnchor, _lineDecorations: IModelDecoration[]): ChatAgentHoverPart[] { + if (!this.editor.hasModel()) { + return []; + } + + const widget = this.chatWidgetService.getWidgetByInputUri(this.editor.getModel().uri); + if (!widget) { + return []; + } + + const { agentPart } = extractAgentAndCommand(widget.parsedInput); + if (!agentPart) { + return []; + } + + if (Range.containsPosition(agentPart.editorRange, anchor.range.getStartPosition())) { + return [new ChatAgentHoverPart(this, Range.lift(agentPart.editorRange), agentPart.agent)]; + } + + return []; + } + + public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: ChatAgentHoverPart[]): IRenderedHoverParts { + if (!hoverParts.length) { + return new RenderedHoverParts([]); + } + + const disposables = new DisposableStore(); + const hover = disposables.add(this.instantiationService.createInstance(ChatAgentHover)); + disposables.add(hover.onDidChangeContents(() => context.onContentsChanged())); + const hoverPart = hoverParts[0]; + const agent = hoverPart.agent; + hover.setAgent(agent.id); + + const actions = getChatAgentHoverOptions(() => agent, this.commandService).actions; + const wrapper = this.instantiationService.createInstance(ChatEditorHoverWrapper, hover.domNode, actions); + const wrapperNode = wrapper.domNode; + context.fragment.appendChild(wrapperNode); + const renderedHoverPart: IRenderedHoverPart = { + hoverPart, + hoverElement: wrapperNode, + dispose() { disposables.dispose(); } + }; + return new RenderedHoverParts([renderedHoverPart]); + } + + public getAccessibleContent(hoverPart: ChatAgentHoverPart): string { + return nls.localize('hoverAccessibilityChatAgent', 'There is a chat agent hover part here.'); + + } +} + +export class ChatAgentHoverPart implements IHoverPart { + + constructor( + public readonly owner: IEditorHoverParticipant, + public readonly range: Range, + public readonly agent: IChatAgentData + ) { } + + public isValidForHoverAnchor(anchor: HoverAnchor): boolean { + return ( + anchor.type === HoverAnchorType.Range + && this.range.startColumn <= anchor.range.startColumn + && this.range.endColumn >= anchor.range.endColumn + ); + } +} + +HoverParticipantRegistry.register(ChatAgentHoverParticipant); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts b/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts new file mode 100644 index 00000000000..5cbc7d932cc --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/editorHoverWrapper'; +import * as dom from 'vs/base/browser/dom'; +import { IHoverAction } from 'vs/base/browser/ui/hover/hover'; +import { HoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; + +const $ = dom.$; +const h = dom.h; + +/** + * This borrows some of HoverWidget so that a chat editor hover can be rendered in the same way as a workbench hover. + * Maybe it can be reusable in a generic way. + */ +export class ChatEditorHoverWrapper { + public readonly domNode: HTMLElement; + + constructor( + hoverContentElement: HTMLElement, + actions: IHoverAction[] | undefined, + @IKeybindingService private readonly keybindingService: IKeybindingService, + ) { + const hoverElement = h( + '.chat-editor-hover-wrapper@root', + [h('.chat-editor-hover-wrapper-content@content')]); + this.domNode = hoverElement.root; + hoverElement.content.appendChild(hoverContentElement); + + if (actions && actions.length > 0) { + const statusBarElement = $('.hover-row.status-bar'); + const actionsElement = $('.actions'); + actions.forEach(action => { + const keybinding = this.keybindingService.lookupKeybinding(action.commandId); + const keybindingLabel = keybinding ? keybinding.getLabel() : null; + HoverAction.render(actionsElement, { + label: action.label, + commandId: action.commandId, + run: e => { + action.run(e); + }, + iconClass: action.iconClass + }, keybindingLabel); + }); + statusBarElement.appendChild(actionsElement); + this.domNode.appendChild(statusBarElement); + } + } +} diff --git a/src/vs/workbench/workbench.desktop.main.nls.js b/src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css similarity index 80% rename from src/vs/workbench/workbench.desktop.main.nls.js rename to src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css index d6a8b487eaf..d95fd395255 100644 --- a/src/vs/workbench/workbench.desktop.main.nls.js +++ b/src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css @@ -3,6 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// NOTE: THIS FILE WILL BE OVERWRITTEN DURING BUILD TIME, DO NOT EDIT - -define([], {}); \ No newline at end of file +.chat-editor-hover-wrapper-content { + padding: 2px 8px; +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 5310cc02f75..23de8d70e1d 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -48,12 +48,13 @@ font-weight: 600; } -.interactive-item-container .header .detail-container { +.interactive-item-container .detail-container { font-size: 12px; color: var(--vscode-descriptionForeground); + overflow: hidden; } -.interactive-item-container .header .detail-container .detail .agentOrSlashCommandDetected A { +.interactive-item-container .detail-container .detail .agentOrSlashCommandDetected A { cursor: pointer; color: var(--vscode-textLink-foreground); } @@ -170,8 +171,8 @@ width: 100%; } -.interactive-item-container .chat-progress-task { - padding-bottom: 8px; +.interactive-item-container > .value .chat-used-context { + margin-bottom: 8px; } .interactive-item-container .value .rendered-markdown table { @@ -341,6 +342,34 @@ margin: 8px 0; } +.interactive-item-container.minimal { + flex-direction: row; +} + +.interactive-item-container.minimal .column.left { + padding-top: 2px; + display: inline-block; + flex-grow: 0; +} + +.interactive-item-container.minimal .column.right { + display: inline-block; + flex-grow: 1; +} + +.interactive-item-container.minimal .user > .username { + display: none; +} + +.interactive-item-container.minimal .detail-container { + font-size: unset; +} + +.interactive-item-container.minimal > .header { + position: absolute; + right: 0; +} + .interactive-session .interactive-input-and-execute-toolbar { display: flex; box-sizing: border-box; @@ -555,6 +584,12 @@ display: block; color: var(--vscode-textLink-foreground); font-size: 12px; + + /* clamp to max 3 lines */ + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; } .interactive-session .interactive-input-part .interactive-input-followups .interactive-session-followups code { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css index 1599c4ffeac..29e38f48cad 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css @@ -22,8 +22,8 @@ outline: 1px solid var(--vscode-chat-requestBorder); } -.monaco-hover .markdown-hover .hover-contents .chat-agent-hover-icon .codicon { - font-size: 23px; +.chat-agent-hover .chat-agent-hover-icon .codicon { + font-size: 23px !important; /* Override workbench hover styles */ display: flex; justify-content: center; align-items: center; @@ -34,7 +34,7 @@ gap: 4px; } -.monaco-hover .chat-agent-hover .chat-agent-hover-publisher .codicon.codicon-extensions-verified-publisher { +.chat-agent-hover .chat-agent-hover-publisher .codicon.codicon-extensions-verified-publisher { color: var(--vscode-extensionIcon-verifiedForeground); } @@ -60,6 +60,10 @@ font-weight: 600; } +.chat-agent-hover-header .chat-agent-hover-details { + font-size: 12px; +} + .chat-agent-hover-extension { display: flex; gap: 6px; diff --git a/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts index 8a57732c95d..449ff1bc2dd 100644 --- a/src/vs/workbench/contrib/chat/common/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/annotations.ts @@ -11,7 +11,7 @@ import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from 'vs/workben export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI -export function annotateSpecialMarkdownContent(response: ReadonlyArray): ReadonlyArray { +export function annotateSpecialMarkdownContent(response: ReadonlyArray): IChatProgressRenderableResponseContent[] { const result: IChatProgressRenderableResponseContent[] = []; for (const item of response) { const previousItem = result[result.length - 1]; diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 43cf0f8e680..08d7013d770 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -27,7 +27,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; -import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService'; //#region agent service, commands etc @@ -61,6 +61,7 @@ export interface IChatAgentData { name: string; fullName?: string; description?: string; + when?: string; extensionId: ExtensionIdentifier; extensionPublisherId: string; /** This is the extension publisher id, or, in the case of a dynamically registered participant (remote agent), whatever publisher name we have for it */ @@ -72,7 +73,6 @@ export interface IChatAgentData { isDynamic?: boolean; metadata: IChatAgentMetadata; slashCommands: IChatAgentCommand[]; - defaultImplicitVariables?: string[]; locations: ChatAgentLocation[]; } @@ -125,6 +125,7 @@ export interface IChatAgentRequest { enableCommandDetection?: boolean; variables: IChatRequestVariableData; location: ChatAgentLocation; + locationData?: IChatLocationData; acceptedConfirmationData?: any[]; rejectedConfirmationData?: any[]; } @@ -173,6 +174,7 @@ export interface IChatAgentService { getAgents(): IChatAgentData[]; getActivatedAgents(): Array; getAgentsByName(name: string): IChatAgentData[]; + agentHasDupeName(id: string): boolean; /** * Get the default agent (only if activated) @@ -193,7 +195,7 @@ export class ChatAgentService implements IChatAgentService { declare _serviceBrand: undefined; - private _agents: IChatAgentEntry[] = []; + private _agents = new Map(); private readonly _onDidChangeAgents = new Emitter(); readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; @@ -221,15 +223,15 @@ export class ChatAgentService implements IChatAgentService { } }; const entry = { data }; - this._agents.push(entry); + this._agents.set(id, entry); return toDisposable(() => { - this._agents = this._agents.filter(a => a !== entry); + this._agents.delete(id); this._onDidChangeAgents.fire(undefined); }); } registerAgentImplementation(id: string, agentImpl: IChatAgentImplementation): IDisposable { - const entry = this._getAgentEntry(id); + const entry = this._agents.get(id); if (!entry) { throw new Error(`Unknown agent: ${JSON.stringify(id)}`); } @@ -258,11 +260,11 @@ export class ChatAgentService implements IChatAgentService { registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { data.isDynamic = true; const agent = { data, impl: agentImpl }; - this._agents.push(agent); + this._agents.set(data.id, agent); this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); return toDisposable(() => { - this._agents = this._agents.filter(a => a !== agent); + this._agents.delete(data.id); this._onDidChangeAgents.fire(undefined); }); } @@ -281,7 +283,7 @@ export class ChatAgentService implements IChatAgentService { } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { - const agent = this._getAgentEntry(id); + const agent = this._agents.get(id); if (!agent?.impl) { throw new Error(`No activated agent with id ${JSON.stringify(id)} registered`); } @@ -302,28 +304,41 @@ export class ChatAgentService implements IChatAgentService { return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data; } - private _getAgentEntry(id: string): IChatAgentEntry | undefined { - return this._agents.find(a => a.data.id === id); + getAgent(id: string): IChatAgentData | undefined { + if (!this._agentIsEnabled(id)) { + return; + } + + return this._agents.get(id)?.data; } - getAgent(id: string): IChatAgentData | undefined { - return this._getAgentEntry(id)?.data; + private _agentIsEnabled(id: string): boolean { + const entry = this._agents.get(id); + return !entry?.data.when || this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(entry.data.when)); } getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { - return this._agents.find(a => getFullyQualifiedId(a.data) === id)?.data; + const agent = Iterable.find(this._agents.values(), a => getFullyQualifiedId(a.data) === id)?.data; + if (agent && !this._agentIsEnabled(agent.id)) { + return; + } + + return agent; } /** * Returns all agent datas that exist- static registered and dynamic ones. */ getAgents(): IChatAgentData[] { - return this._agents.map(entry => entry.data); + return Array.from(this._agents.values()) + .map(entry => entry.data) + .filter(a => this._agentIsEnabled(a.id)); } getActivatedAgents(): IChatAgent[] { return Array.from(this._agents.values()) .filter(a => !!a.impl) + .filter(a => this._agentIsEnabled(a.data.id)) .map(a => new MergedChatAgent(a.data, a.impl!)); } @@ -331,8 +346,18 @@ export class ChatAgentService implements IChatAgentService { return this.getAgents().filter(a => a.name === name); } + agentHasDupeName(id: string): boolean { + const agent = this.getAgent(id); + if (!agent) { + return false; + } + + return this.getAgentsByName(agent.name) + .filter(a => a.extensionId.value !== agent.extensionId.value).length > 0; + } + async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - const data = this._getAgentEntry(id); + const data = this._agents.get(id); if (!data?.impl) { throw new Error(`No activated agent with id "${id}"`); } @@ -341,7 +366,7 @@ export class ChatAgentService implements IChatAgentService { } async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - const data = this._getAgentEntry(id); + const data = this._agents.get(id); if (!data?.impl) { throw new Error(`No activated agent with id "${id}"`); } @@ -371,7 +396,6 @@ export class MergedChatAgent implements IChatAgent { get isDefault(): boolean | undefined { return this.data.isDefault; } get metadata(): IChatAgentMetadata { return this.data.metadata; } get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; } - get defaultImplicitVariables(): string[] | undefined { return this.data.defaultImplicitVariables; } get locations(): ChatAgentLocation[] { return this.data.locations; } async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/chat/common/chatColors.ts b/src/vs/workbench/contrib/chat/common/chatColors.ts index 3c4cce05e01..15451f0de58 100644 --- a/src/vs/workbench/contrib/chat/common/chatColors.ts +++ b/src/vs/workbench/contrib/chat/common/chatColors.ts @@ -39,6 +39,6 @@ export const chatAvatarBackground = registerColor( export const chatAvatarForeground = registerColor( 'chat.avatarForeground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground, }, + foreground, localize('chat.avatarForeground', 'The foreground color of a chat avatar.') ); diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 9aea7295a4a..bd51f83ce09 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -27,3 +27,4 @@ export const CONTEXT_CHAT_ENABLED = new RawContextKey('chatIsEnabled', export const CONTEXT_CHAT_INPUT_CURSOR_AT_TOP = new RawContextKey('chatCursorAtTop', false); export const CONTEXT_CHAT_INPUT_HAS_AGENT = new RawContextKey('chatInputHasAgent', false); export const CONTEXT_CHAT_LOCATION = new RawContextKey('chatLocation', undefined); +export const CONTEXT_IN_QUICK_CHAT = new RawContextKey('quickChatHasFocus', false, { type: 'boolean', description: localize('inQuickChat', "True when the quick chat UI has focus, false otherwise.") }); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 7338d036535..b7c992fe5ae 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { asArray, firstOrDefault } from 'vs/base/common/arrays'; import { DeferredPromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -33,6 +34,7 @@ export interface IChatRequestVariableEntry { value: IChatRequestVariableValue; references?: IChatContentReference[]; isDynamic?: boolean; + isFile?: boolean; } export interface IChatRequestVariableData { @@ -50,13 +52,15 @@ export interface IChatRequestModel { readonly response?: IChatResponseModel; } +export interface IChatTextEditGroupState { + sha1: string; + applied: number; +} + export interface IChatTextEditGroup { uri: URI; edits: TextEdit[][]; - state?: { - sha1: string; - applied: number; - }; + state?: IChatTextEditGroupState; kind: 'textEditGroup'; } @@ -76,7 +80,8 @@ export type IChatProgressRenderableResponseContent = Exclude; - asString(): string; + toMarkdown(): string; + toString(): string; } export interface IChatResponseModel { @@ -155,10 +160,18 @@ export class Response implements IResponse { return this._onDidChangeValue.event; } - // responseParts internally tracks all the response parts, including strings which are currently resolving, so that they can be updated when they do resolve private _responseParts: IChatProgressResponseContent[]; - // responseRepr externally presents the response parts with consolidated contiguous strings (excluding tree data) - private _responseRepr!: string; + + /** + * A stringified representation of response data which might be presented to a screenreader or used when copying a response. + */ + private _responseRepr = ''; + + /** + * Just the markdown content of the response, used for determining the rendering rate of markdown + */ + private _markdownContent = ''; + get value(): IChatProgressResponseContent[] { return this._responseParts; @@ -172,10 +185,14 @@ export class Response implements IResponse { this._updateRepr(true); } - asString(): string { + toString(): string { return this._responseRepr; } + toMarkdown(): string { + return this._markdownContent; + } + clear(): void { this._responseParts = []; this._updateRepr(true); @@ -228,7 +245,7 @@ export class Response implements IResponse { // Replace the resolving part's content with the resolved response if (typeof content === 'string') { - this._responseParts[responsePosition] = { ...progress, content: new MarkdownString(content) }; + (this._responseParts[responsePosition] as IChatTask).content = new MarkdownString(content); } this._updateRepr(false); }); @@ -248,7 +265,7 @@ export class Response implements IResponse { } else if (part.kind === 'command') { return part.command.title; } else if (part.kind === 'textEditGroup') { - return ''; + return localize('editsSummary', "Made changes."); } else if (part.kind === 'progressMessage') { return ''; } else if (part.kind === 'confirmation') { @@ -260,6 +277,18 @@ export class Response implements IResponse { .filter(s => s.length > 0) .join('\n\n'); + this._markdownContent = this._responseParts.map(part => { + if (part.kind === 'inlineReference') { + return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); + } else if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') { + return part.content.value; + } else { + return ''; + } + }) + .filter(s => s.length > 0) + .join('\n\n'); + if (!quiet) { this._onDidChangeValue.fire(); } @@ -525,10 +554,28 @@ export interface IChatAddResponseEvent { response: IChatResponseModel; } +export const enum ChatRequestRemovalReason { + /** + * "Normal" remove + */ + Removal, + + /** + * Removed because the request will be resent + */ + Resend, + + /** + * Remove because the request is moving to another model + */ + Adoption +} + export interface IChatRemoveRequestEvent { kind: 'removeRequest'; requestId: string; responseId?: string; + reason: ChatRequestRemovalReason; } export interface IChatInitEvent { @@ -703,7 +750,8 @@ export class ChatModel extends Disposable implements IChatModel { { variables: [] }; variableData.variables = variableData.variables.map((v): IChatRequestVariableEntry => { - if ('values' in v && Array.isArray(v.values)) { + // Old variables format + if (v && 'values' in v && Array.isArray(v.values)) { return { id: v.id ?? '', name: v.name, @@ -800,7 +848,7 @@ export class ChatModel extends Disposable implements IChatModel { request.response?.adoptTo(this); this._requests.push(request); - oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id }); + oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason: ChatRequestRemovalReason.Adoption }); this._onDidChange.fire({ kind: 'addRequest', request }); } @@ -837,12 +885,12 @@ export class ChatModel extends Disposable implements IChatModel { } } - removeRequest(id: string): void { + removeRequest(id: string, reason: ChatRequestRemovalReason = ChatRequestRemovalReason.Removal): void { const index = this._requests.findIndex(request => request.id === id); const request = this._requests[index]; if (index !== -1) { - this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id }); + this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason }); this._requests.splice(index, 1); request.response?.dispose(); } diff --git a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts index e6d695c3824..638dce2633c 100644 --- a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts @@ -18,6 +18,7 @@ export interface IRawChatParticipantContribution { id: string; name: string; fullName: string; + when?: string; description?: string; isDefault?: boolean; isSticky?: boolean; diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index 73276b9464d..e6492570295 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -43,7 +43,7 @@ export class ChatRequestParser { } else if (char === chatAgentLeader) { newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context); } else if (char === chatSubcommandLeader) { - newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts); + newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts, location); } if (!newPart) { @@ -90,7 +90,7 @@ export class ChatRequestParser { }; } - private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | ChatRequestVariablePart | undefined { + private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: Array, location: ChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | ChatRequestVariablePart | undefined { const nextAgentMatch = message.match(agentReg); if (!nextAgentMatch) { return; @@ -159,7 +159,7 @@ export class ChatRequestParser { return; } - private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { + private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { const nextSlashMatch = remainingMessage.match(slashReg); if (!nextSlashMatch) { return; @@ -194,11 +194,19 @@ export class ChatRequestParser { return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); } } else { - const slashCommands = this.slashCommandService.getCommands(); + const slashCommands = this.slashCommandService.getCommands(location); const slashCommand = slashCommands.find(c => c.command === command); if (slashCommand) { // Valid standalone slash command return new ChatRequestSlashCommandPart(slashRange, slashEditorRange, slashCommand); + } else { + // check for with default agent for this location + const defaultAgent = this.agentService.getDefaultAgent(location); + const subCommand = defaultAgent?.slashCommands.find(c => c.name === command); + if (subCommand) { + // Valid default agent subcommand + return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); + } } } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index a6921314d18..f10564d7828 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -10,6 +10,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IRange, Range } from 'vs/editor/common/core/range'; +import { ISelection } from 'vs/editor/common/core/selection'; import { Command, Location, TextEdit } from 'vs/editor/common/languages'; import { FileType } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -251,6 +252,7 @@ export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertActi export interface IChatUserActionEvent { action: ChatUserAction; agentId: string | undefined; + command: string | undefined; sessionId: string; requestId: string; result: IChatAgentResult | undefined; @@ -298,8 +300,28 @@ export interface IChatSendRequestData extends IChatSendRequestResponseState { slashCommand?: IChatAgentCommand; } +export interface IChatEditorLocationData { + type: ChatAgentLocation.Editor; + document: URI; + selection: ISelection; + wholeRange: IRange; +} + +export interface IChatNotebookLocationData { + type: ChatAgentLocation.Notebook; + sessionInputUri: URI; +} + +export interface IChatTerminalLocationData { + type: ChatAgentLocation.Terminal; + // TBD +} + +export type IChatLocationData = IChatEditorLocationData | IChatNotebookLocationData | IChatTerminalLocationData; + export interface IChatSendRequestOptions { location?: ChatAgentLocation; + locationData?: IChatLocationData; parserContext?: IChatParserContext; attempt?: number; noCommandDetection?: boolean; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 06dbe34e911..b18e59ac6b4 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce } from 'vs/base/common/arrays'; import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -16,7 +15,6 @@ import { revive } from 'vs/base/common/marshalling'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { Progress } from 'vs/platform/progress/common/progress'; @@ -24,10 +22,11 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { ChatCopyKind, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatServiceTelemetry } from 'vs/workbench/contrib/chat/common/chatServiceTelemetry'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; @@ -68,64 +67,6 @@ type ChatProviderInvokedClassification = { comment: 'Provides insight into the performance of Chat agents.'; }; -type ChatVoteEvent = { - direction: 'up' | 'down'; - agentId: string; -}; - -type ChatVoteClassification = { - direction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user voted up or down.' }; - agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this vote is for.' }; - owner: 'roblourens'; - comment: 'Provides insight into the performance of Chat agents.'; -}; - -type ChatCopyEvent = { - copyKind: 'action' | 'toolbar'; - agentId: string; -}; - -type ChatCopyClassification = { - copyKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the copy was initiated.' }; - agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that the copy acted on.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; -}; - -type ChatInsertEvent = { - newFile: boolean; - agentId: string; -}; - -type ChatInsertClassification = { - newFile: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code was inserted into a new untitled file.' }; - agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this insertion is for.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; -}; - -type ChatCommandEvent = { - commandId: string; - agentId: string; -}; - -type ChatCommandClassification = { - commandId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the command that was executed.' }; - agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; -}; - -type ChatTerminalEvent = { - languageId: string; -}; - -type ChatTerminalClassification = { - languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language of the code that was run in the terminal.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; -}; - const maxPersistedSessions = 25; export class ChatService extends Disposable implements IChatService { @@ -148,6 +89,7 @@ export class ChatService extends Disposable implements IChatService { public readonly onDidDisposeSession = this._onDidDisposeSession.event; private readonly _sessionFollowupCancelTokens = this._register(new DisposableMap()); + private readonly _chatServiceTelemetry: ChatServiceTelemetry; constructor( @IStorageService private readonly storageService: IStorageService, @@ -162,6 +104,7 @@ export class ChatService extends Disposable implements IChatService { ) { super(); + this._chatServiceTelemetry = this.instantiationService.createInstance(ChatServiceTelemetry); const sessionData = storageService.get(serializedChatKey, StorageScope.WORKSPACE, ''); if (sessionData) { this._persistedSessions = this.deserializeChats(sessionData); @@ -212,35 +155,7 @@ export class ChatService extends Disposable implements IChatService { } notifyUserAction(action: IChatUserActionEvent): void { - if (action.action.kind === 'vote') { - this.telemetryService.publicLog2('interactiveSessionVote', { - direction: action.action.direction === ChatAgentVoteDirection.Up ? 'up' : 'down', - agentId: action.agentId ?? '' - }); - } else if (action.action.kind === 'copy') { - this.telemetryService.publicLog2('interactiveSessionCopy', { - copyKind: action.action.copyKind === ChatCopyKind.Action ? 'action' : 'toolbar', - agentId: action.agentId ?? '' - }); - } else if (action.action.kind === 'insert') { - this.telemetryService.publicLog2('interactiveSessionInsert', { - newFile: !!action.action.newFile, - agentId: action.agentId ?? '' - }); - } else if (action.action.kind === 'command') { - // TODO not currently called - const command = CommandsRegistry.getCommand(action.action.commandButton.command.id); - const commandId = command ? action.action.commandButton.command.id : 'INVALID'; - this.telemetryService.publicLog2('interactiveSessionCommand', { - commandId, - agentId: action.agentId ?? '' - }); - } else if (action.action.kind === 'runInTerminal') { - this.telemetryService.publicLog2('interactiveSessionRunInTerminal', { - languageId: action.action.languageId ?? '' - }); - } - + this._chatServiceTelemetry.notifyUserAction(action); this._onDidPerformUserAction.fire(action); } @@ -394,7 +309,7 @@ export class ChatService extends Disposable implements IChatService { return model; } - const sessionData = this._persistedSessions[sessionId]; + const sessionData = revive(this._persistedSessions[sessionId]); if (!sessionData) { return undefined; } @@ -418,9 +333,10 @@ export class ChatService extends Disposable implements IChatService { await model.waitForInitialization(); - if (this._pendingRequests.has(request.session.sessionId)) { - this.trace('sendRequest', `Session ${request.session.sessionId} already has a pending request`); - return; + const cts = this._pendingRequests.get(request.session.sessionId); + if (cts) { + this.trace('resendRequest', `Session ${request.session.sessionId} already has a pending request, cancelling...`); + cts.cancel(); } const location = options?.location ?? model.initialLocation; @@ -428,7 +344,7 @@ export class ChatService extends Disposable implements IChatService { const enableCommandDetection = !options?.noCommandDetection; const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; - this.removeRequest(model.sessionId, request.id); + model.removeRequest(request.id, ChatRequestRemovalReason.Resend); await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, options).responseCompletePromise; } @@ -568,21 +484,6 @@ export class ChatService extends Disposable implements IChatService { const promptTextResult = getPromptText(request.message); const updatedVariableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack - // TODO- should figure out how to get rid of implicit variables for inline chat - const implicitVariablesEnabled = (location === ChatAgentLocation.Editor || location === ChatAgentLocation.Notebook); - if (implicitVariablesEnabled) { - const implicitVariables = agent.defaultImplicitVariables; - if (implicitVariables) { - const resolvedImplicitVariables = await Promise.all(implicitVariables.map(async v => { - const id = this.chatVariablesService.getVariable(v)?.id ?? ''; - const value = await this.chatVariablesService.resolveVariable(v, parsedRequest.text, model, progressCallback, token); - return value ? { id, name: v, value } satisfies IChatRequestVariableEntry : - undefined; - })); - updatedVariableData.variables.push(...coalesce(resolvedImplicitVariables)); - } - } - const requestProps: IChatAgentRequest = { sessionId, requestId: request.id, @@ -593,6 +494,7 @@ export class ChatService extends Disposable implements IChatService { enableCommandDetection, attempt, location, + locationData: options?.locationData, acceptedConfirmationData: options?.acceptedConfirmationData, rejectedConfirmationData: options?.rejectedConfirmationData, }; @@ -610,13 +512,13 @@ export class ChatService extends Disposable implements IChatService { if (!request.response) { continue; } - history.push({ role: ChatMessageRole.User, content: request.message.text }); - history.push({ role: ChatMessageRole.Assistant, content: request.response.response.asString() }); + history.push({ role: ChatMessageRole.User, content: { type: 'text', value: request.message.text } }); + history.push({ role: ChatMessageRole.Assistant, content: { type: 'text', value: request.response.response.toString() } }); } const message = parsedRequest.text; const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { progressCallback(p); - }), history, token); + }), history, location, token); agentOrCommandFollowups = Promise.resolve(commandResult?.followUp); rawResult = {}; @@ -669,11 +571,13 @@ export class ChatService extends Disposable implements IChatService { chatSessionId: model.sessionId, location }); - const rawResult: IChatAgentResult = { errorDetails: { message: err.message } }; - model.setResponse(request, rawResult); - completeResponseCreated(); - this.trace('sendRequest', `Error while handling request: ${toErrorMessage(err)}`); - model.completeResponse(request); + this.logService.error(`Error while handling chat request: ${toErrorMessage(err, true)}`); + if (request) { + const rawResult: IChatAgentResult = { errorDetails: { message: err.message } }; + model.setResponse(request, rawResult); + completeResponseCreated(); + model.completeResponse(request); + } } finally { listener.dispose(); } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts b/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts new file mode 100644 index 00000000000..ec8d5aa339e --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IChatUserActionEvent, ChatAgentVoteDirection, ChatCopyKind } from 'vs/workbench/contrib/chat/common/chatService'; + +type ChatVoteEvent = { + direction: 'up' | 'down'; + agentId: string; + command: string | undefined; +}; + +type ChatVoteClassification = { + direction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user voted up or down.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this vote is for.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the slash command that this vote is for.' }; + owner: 'roblourens'; + comment: 'Provides insight into the performance of Chat agents.'; +}; + +type ChatCopyEvent = { + copyKind: 'action' | 'toolbar'; + agentId: string; + command: string | undefined; +}; + +type ChatCopyClassification = { + copyKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the copy was initiated.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that the copy acted on.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the slash command the copy acted on.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + +type ChatInsertEvent = { + newFile: boolean; + agentId: string; + command: string | undefined; +}; + +type ChatInsertClassification = { + newFile: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code was inserted into a new untitled file.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this insertion is for.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the slash command that this insertion is for.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + +type ChatCommandEvent = { + commandId: string; + agentId: string; + command: string | undefined; +}; + +type ChatCommandClassification = { + commandId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the command that was executed.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the related slash command.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + +type ChatTerminalEvent = { + languageId: string; + agentId: string; + command: string | undefined; +}; + +type ChatTerminalClassification = { + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language of the code that was run in the terminal.' }; + agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' }; + command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the related slash command.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of Chat features.'; +}; + +export class ChatServiceTelemetry { + constructor( + @ITelemetryService private readonly telemetryService: ITelemetryService, + ) { } + + notifyUserAction(action: IChatUserActionEvent): void { + if (action.action.kind === 'vote') { + this.telemetryService.publicLog2('interactiveSessionVote', { + direction: action.action.direction === ChatAgentVoteDirection.Up ? 'up' : 'down', + agentId: action.agentId ?? '', + command: action.command, + }); + } else if (action.action.kind === 'copy') { + this.telemetryService.publicLog2('interactiveSessionCopy', { + copyKind: action.action.copyKind === ChatCopyKind.Action ? 'action' : 'toolbar', + agentId: action.agentId ?? '', + command: action.command, + }); + } else if (action.action.kind === 'insert') { + this.telemetryService.publicLog2('interactiveSessionInsert', { + newFile: !!action.action.newFile, + agentId: action.agentId ?? '', + command: action.command, + }); + } else if (action.action.kind === 'command') { + // TODO not currently called + const command = CommandsRegistry.getCommand(action.action.commandButton.command.id); + const commandId = command ? action.action.commandButton.command.id : 'INVALID'; + this.telemetryService.publicLog2('interactiveSessionCommand', { + commandId, + agentId: action.agentId ?? '', + command: action.command, + }); + } else if (action.action.kind === 'runInTerminal') { + this.telemetryService.publicLog2('interactiveSessionRunInTerminal', { + languageId: action.action.languageId ?? '', + agentId: action.agentId ?? '', + command: action.command, + }); + } + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts index 2d43f1c0396..3b19cd512b9 100644 --- a/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts @@ -11,6 +11,7 @@ import { IProgress } from 'vs/platform/progress/common/progress'; import { IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; import { IChatFollowup, IChatProgress, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; //#region slash service, commands etc @@ -18,18 +19,18 @@ export interface IChatSlashData { command: string; detail: string; sortText?: string; - /** * Whether the command should execute as soon * as it is entered. Defaults to `false`. */ executeImmediately?: boolean; + locations: ChatAgentLocation[]; } export interface IChatSlashFragment { content: string | { treeData: IChatResponseProgressFileTreeData }; } -export type IChatSlashCallback = { (prompt: string, progress: IProgress, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> }; +export type IChatSlashCallback = { (prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> }; export const IChatSlashCommandService = createDecorator('chatSlashCommandService'); @@ -40,8 +41,8 @@ export interface IChatSlashCommandService { _serviceBrand: undefined; readonly onDidChangeCommands: Event; registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable; - executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>; - getCommands(): Array; + executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>; + getCommands(location: ChatAgentLocation): Array; hasCommand(id: string): boolean; } @@ -80,15 +81,15 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom }); } - getCommands(): Array { - return Array.from(this._commands.values(), v => v.data); + getCommands(location: ChatAgentLocation): Array { + return Array.from(this._commands.values(), v => v.data).filter(c => c.locations.includes(location)); } hasCommand(id: string): boolean { return this._commands.has(id); } - async executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> { + async executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> { const data = this._commands.get(id); if (!data) { throw new Error('No command with id ${id} NOT registered'); @@ -100,6 +101,6 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom throw new Error(`No command with id ${id} NOT resolved`); } - return await data.command(prompt, progress, history, token); + return await data.command(prompt, progress, history, location, token); } } diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 130ef76979b..e4e742afcd5 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -13,9 +13,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/annotations'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModelInitState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatAgentVoteDirection, IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTask, IChatUsedContext, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; @@ -78,6 +78,13 @@ export interface IChatResponseMarkdownRenderData { originalMarkdown: IMarkdownString; } +export interface IChatResponseMarkdownRenderData2 { + renderedWordCount: number; + lastRenderTime: number; + isFullyRendered: boolean; + originalMarkdown: IMarkdownString; +} + export interface IChatProgressMessageRenderData { progressMessage: IChatProgressMessage; @@ -101,13 +108,28 @@ export interface IChatTaskRenderData { progressLength: number; } -export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation | IChatTaskRenderData | IChatWarningMessage; export interface IChatResponseRenderData { - renderedParts: IChatRenderData[]; + renderedParts: IChatRendererContent[]; + + renderedWordCount: number; + lastRenderTime: number; } +/** + * Content type for references used during rendering, not in the model + */ +export interface IChatReferences { + references: ReadonlyArray; + kind: 'references'; +} + +/** + * Type for content parts rendered by IChatListRenderer + */ +export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences; + export interface IChatLiveUpdateData { - loadingStartTime: number; + firstWordTime: number; lastUpdateTime: number; impliedWordLoadRate: number; lastWordCount: number; @@ -484,7 +506,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi if (!_model.isComplete) { this._contentUpdateTimings = { - loadingStartTime: Date.now(), + firstWordTime: 0, lastUpdateTime: Date.now(), impliedWordLoadRate: 0, lastWordCount: 0 @@ -492,15 +514,17 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi } this._register(_model.onDidChange(() => { + // This should be true, if the model is changing if (this._contentUpdateTimings) { - // This should be true, if the model is changing const now = Date.now(); - const wordCount = countWords(_model.response.asString()); - const timeDiff = now - this._contentUpdateTimings.loadingStartTime; + const wordCount = countWords(_model.response.toString()); + + // Apply a min time difference, or the rate is typically too high for first few words + const timeDiff = Math.max(now - this._contentUpdateTimings.firstWordTime, 250); const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (timeDiff / 1000); - this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); + this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over last ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); this._contentUpdateTimings = { - loadingStartTime: this._contentUpdateTimings.loadingStartTime, + firstWordTime: this._contentUpdateTimings.firstWordTime === 0 && this.response.value.some(v => v.kind === 'markdownContent') ? now : this._contentUpdateTimings.firstWordTime, lastUpdateTime: now, impliedWordLoadRate, lastWordCount: wordCount diff --git a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts index 94870296160..b81d391186e 100644 --- a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts +++ b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts @@ -5,13 +5,18 @@ export interface IWordCountResult { value: string; - actualWordCount: number; + returnedWordCount: number; + totalWordCount: number; isFullString: boolean; } export function getNWords(str: string, numWordsToCount: number): IWordCountResult { - // Match words and markdown style links - const allWordMatches = Array.from(str.matchAll(/\[([^\]]+)\]\(([^)]+)\)|[^\s\|\-]+/g)); + // This regex matches each word and skips over whitespace and separators. A word is: + // A markdown link + // One chinese character + // One or more + - =, handled so that code like "a=1+2-3" is broken up better + // One or more characters that aren't whitepace or any of the above + const allWordMatches = Array.from(str.matchAll(/\[([^\]]+)\]\(([^)]+)\)|\p{sc=Han}|=+|\++|-+|[^\s\|\p{sc=Han}|=|\+|\-]+/gu)); const targetWords = allWordMatches.slice(0, numWordsToCount); @@ -22,12 +27,13 @@ export function getNWords(str: string, numWordsToCount: number): IWordCountResul const value = str.substring(0, endIndex); return { value, - actualWordCount: targetWords.length === 0 ? (value.length ? 1 : 0) : targetWords.length, - isFullString: endIndex >= str.length + returnedWordCount: targetWords.length === 0 ? (value.length ? 1 : 0) : targetWords.length, + isFullString: endIndex >= str.length, + totalWordCount: allWordMatches.length }; } export function countWords(str: string): number { const result = getNWords(str, Number.MAX_SAFE_INTEGER); - return result.actualWordCount; + return result.returnedWordCount; } diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index a857b64cb1d..d5ed699cd9b 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -22,6 +22,13 @@ export class CodeBlockModelCollection extends Disposable { vulns: readonly IMarkdownVulnerability[]; }>(); + /** + * Max number of models to keep in memory. + * + * Currently always maintains the most recently created models. + */ + private readonly maxModelCount = 100; + constructor( @ILanguageService private readonly languageService: ILanguageService, @ITextModelService private readonly textModelService: ITextModelService @@ -52,9 +59,28 @@ export class CodeBlockModelCollection extends Disposable { const uri = this.getUri(sessionId, chat, codeBlockIndex); const ref = this.textModelService.createModelReference(uri); this._models.set(uri, { model: ref, vulns: [] }); + + while (this._models.size > this.maxModelCount) { + const first = Array.from(this._models.keys()).at(0); + if (!first) { + break; + } + this.delete(first); + } + return { model: ref.then(ref => ref.object), vulns: [] }; } + private delete(codeBlockUri: URI) { + const entry = this._models.get(codeBlockUri); + if (!entry) { + return; + } + + entry.model.then(ref => ref.dispose()); + this._models.delete(codeBlockUri); + } + clear(): void { this._models.forEach(async entry => (await entry.model).dispose()); this._models.clear(); diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts new file mode 100644 index 00000000000..a1c70e6d1a4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Iterable } from 'vs/base/common/iterator'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export interface IToolData { + name: string; + displayName?: string; + description: string; + parametersSchema?: Object; +} + +interface IToolEntry { + data: IToolData; + impl?: IToolImpl; +} + +export interface IToolImpl { + invoke(parameters: any, token: CancellationToken): Promise; +} + +export const ILanguageModelToolsService = createDecorator('ILanguageModelToolsService'); + +export interface IToolDelta { + added?: IToolData; + removed?: string; +} + +export interface ILanguageModelToolsService { + _serviceBrand: undefined; + onDidChangeTools: Event; + registerToolData(toolData: IToolData): IDisposable; + registerToolImplementation(name: string, tool: IToolImpl): IDisposable; + getTools(): Iterable>; + invokeTool(name: string, parameters: any, token: CancellationToken): Promise; +} + +export class LanguageModelToolsService implements ILanguageModelToolsService { + _serviceBrand: undefined; + + private _onDidChangeTools = new Emitter(); + readonly onDidChangeTools = this._onDidChangeTools.event; + + private _tools = new Map(); + + constructor( + @IExtensionService private readonly _extensionService: IExtensionService + ) { } + + registerToolData(toolData: IToolData): IDisposable { + if (this._tools.has(toolData.name)) { + throw new Error(`Tool "${toolData.name}" is already registered.`); + } + + this._tools.set(toolData.name, { data: toolData }); + this._onDidChangeTools.fire({ added: toolData }); + + return toDisposable(() => { + this._tools.delete(toolData.name); + this._onDidChangeTools.fire({ removed: toolData.name }); + }); + + } + + registerToolImplementation(name: string, tool: IToolImpl): IDisposable { + const entry = this._tools.get(name); + if (!entry) { + throw new Error(`Tool "${name}" was not contributed.`); + } + + if (entry.impl) { + throw new Error(`Tool "${name}" already has an implementation.`); + } + + entry.impl = tool; + return toDisposable(() => { + entry.impl = undefined; + }); + } + + getTools(): Iterable> { + return Iterable.map(this._tools.values(), i => i.data); + } + + async invokeTool(name: string, parameters: any, token: CancellationToken): Promise { + let tool = this._tools.get(name); + if (!tool) { + throw new Error(`Tool ${name} was not contributed`); + } + + if (!tool.impl) { + await this._extensionService.activateByEvent(`onLanguageModelTool:${name}`); + + // Extension should activate and register the tool implementation + tool = this._tools.get(name); + if (!tool?.impl) { + throw new Error(`Tool ${name} does not have an implementation registered.`); + } + } + + return tool.impl.invoke(parameters, token); + } +} diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index bd16b27c47f..b19a50e5e44 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -7,13 +7,12 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgress } from 'vs/platform/progress/common/progress'; import { IExtensionService, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -23,14 +22,42 @@ export const enum ChatMessageRole { Assistant, } -export interface IChatMessage { - readonly role: ChatMessageRole; - readonly content: string; +export interface IChatMessageTextPart { + type: 'text'; + value: string; } +export interface IChatMessageFunctionResultPart { + type: 'function_result'; + name: string; + value: any; + isError?: boolean; +} + +export type IChatMessagePart = IChatMessageTextPart | IChatMessageFunctionResultPart; + +export interface IChatMessage { + readonly name?: string | undefined; + readonly role: ChatMessageRole; + readonly content: IChatMessagePart; +} + +export interface IChatResponseTextPart { + type: 'text'; + value: string; +} + +export interface IChatResponceFunctionUsePart { + type: 'function_use'; + name: string; + parameters: any; +} + +export type IChatResponsePart = IChatResponseTextPart | IChatResponceFunctionUsePart; + export interface IChatResponseFragment { index: number; - part: string; + part: IChatResponsePart; } export interface ILanguageModelChatMetadata { @@ -51,9 +78,14 @@ export interface ILanguageModelChatMetadata { }; } +export interface ILanguageModelChatResponse { + stream: AsyncIterable; + result: Promise; +} + export interface ILanguageModelChat { metadata: ILanguageModelChatMetadata; - provideChatResponse(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise; + sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise; provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise; } @@ -91,7 +123,7 @@ export interface ILanguageModelsService { registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable; - makeLanguageModelChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise; + sendChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; computeTokenLength(identifier: string, message: string | IChatMessage, token: CancellationToken): Promise; } @@ -133,10 +165,12 @@ export class LanguageModelsService implements ILanguageModelsService { readonly _serviceBrand: undefined; + private readonly _store = new DisposableStore(); + private readonly _providers = new Map(); private readonly _vendors = new Set(); - private readonly _onDidChangeProviders = new Emitter(); + private readonly _onDidChangeProviders = this._store.add(new Emitter()); readonly onDidChangeLanguageModels: Event = this._onDidChangeProviders.event; constructor( @@ -144,7 +178,7 @@ export class LanguageModelsService implements ILanguageModelsService { @ILogService private readonly _logService: ILogService, ) { - languageModelExtensionPoint.setHandler((extensions) => { + this._store.add(languageModelExtensionPoint.setHandler((extensions) => { this._vendors.clear(); @@ -182,11 +216,11 @@ export class LanguageModelsService implements ILanguageModelsService { if (removed.length > 0) { this._onDidChangeProviders.fire({ removed }); } - }); + })); } dispose() { - this._onDidChangeProviders.dispose(); + this._store.dispose(); this._providers.clear(); } @@ -213,22 +247,12 @@ export class LanguageModelsService implements ILanguageModelsService { for (const [identifier, model] of this._providers) { - if (selector.vendor !== undefined && model.metadata.vendor === selector.vendor - || selector.family !== undefined && model.metadata.family === selector.family - || selector.version !== undefined && model.metadata.version === selector.version - || selector.identifier !== undefined && model.metadata.id === selector.identifier - || selector.extension !== undefined && model.metadata.targetExtensions?.some(candidate => ExtensionIdentifier.equals(candidate, selector.extension)) + if ((selector.vendor === undefined || model.metadata.vendor === selector.vendor) + && (selector.family === undefined || model.metadata.family === selector.family) + && (selector.version === undefined || model.metadata.version === selector.version) + && (selector.identifier === undefined || model.metadata.id === selector.identifier) + && (!model.metadata.targetExtensions || model.metadata.targetExtensions.some(candidate => ExtensionIdentifier.equals(candidate, selector.extension))) ) { - // true selection - result.push(identifier); - - } else if (!selector || ( - selector.vendor === undefined - && selector.family === undefined - && selector.version === undefined - && selector.identifier === undefined) - ) { - // no selection result.push(identifier); } } @@ -258,12 +282,12 @@ export class LanguageModelsService implements ILanguageModelsService { }); } - makeLanguageModelChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise { + async sendChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise { const provider = this._providers.get(identifier); if (!provider) { throw new Error(`Chat response provider with identifier ${identifier} is not registered.`); } - return provider.provideChatResponse(messages, from, options, progress, token); + return provider.sendChatRequest(messages, from, options, token); } computeTokenLength(identifier: string, message: string | IChatMessage, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts new file mode 100644 index 00000000000..5cc34e72faf --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { DisposableMap } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { ILanguageModelToolsService } from 'vs/workbench/contrib/chat/common/languageModelToolsService'; +import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; + +interface IRawToolContribution { + name: string; + displayName?: string; + description: string; + parametersSchema?: IJSONSchema; +} + +const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'languageModelTools', + activationEventsGenerator: (contributions: IRawToolContribution[], result) => { + for (const contrib of contributions) { + result.push(`onLanguageModelTool:${contrib.name}`); + } + }, + jsonSchema: { + description: localize('vscode.extension.contributes.tools', 'Contributes a tool that can be invoked by a language model.'), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name', 'description'], + properties: { + name: { + description: localize('toolname', "A name for this tool which must be unique across all tools."), + type: 'string' + }, + description: { + description: localize('toolDescription', "A description of this tool that may be passed to a language model."), + type: 'string' + }, + displayName: { + description: localize('toolDisplayName', "A human-readable name for this tool that may be used to describe it in the UI."), + type: 'string' + }, + parametersSchema: { + description: localize('parametersSchema', "A JSON schema for the parameters this tool accepts."), + type: 'object', + $ref: 'http://json-schema.org/draft-07/schema#' + } + } + } + } +}); + +function toToolKey(extensionIdentifier: ExtensionIdentifier, toolName: string) { + return `${extensionIdentifier.value}/${toolName}`; +} + +export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.toolsExtensionPointHandler'; + + private _registrationDisposables = new DisposableMap(); + + constructor( + @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, + @ILogService logService: ILogService, + ) { + languageModelToolsExtensionPoint.setHandler((extensions, delta) => { + for (const extension of delta.added) { + for (const tool of extension.value) { + if (!tool.name || !tool.description) { + logService.warn(`Invalid tool contribution from ${extension.description.identifier.value}: ${JSON.stringify(tool)}`); + continue; + } + + const disposable = languageModelToolsService.registerToolData(tool); + this._registrationDisposables.set(toToolKey(extension.description.identifier, tool.name), disposable); + } + } + + for (const extension of delta.removed) { + for (const tool of extension.value) { + this._registrationDisposables.deleteAndDispose(toToolKey(extension.description.identifier, tool.name)); + } + } + }); + } +} diff --git a/src/vs/workbench/contrib/chat/common/voiceChatService.ts b/src/vs/workbench/contrib/chat/common/voiceChatService.ts index ac140ad3c6b..ae70f2f56a1 100644 --- a/src/vs/workbench/contrib/chat/common/voiceChatService.ts +++ b/src/vs/workbench/contrib/chat/common/voiceChatService.ts @@ -70,13 +70,13 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { private static readonly COMMAND_PREFIX = chatSubcommandLeader; private static readonly PHRASES_LOWER = { - [VoiceChatService.AGENT_PREFIX]: 'at', - [VoiceChatService.COMMAND_PREFIX]: 'slash' + [this.AGENT_PREFIX]: 'at', + [this.COMMAND_PREFIX]: 'slash' }; private static readonly PHRASES_UPPER = { - [VoiceChatService.AGENT_PREFIX]: 'At', - [VoiceChatService.COMMAND_PREFIX]: 'Slash' + [this.AGENT_PREFIX]: 'At', + [this.COMMAND_PREFIX]: 'Slash' }; private static readonly CHAT_AGENT_ALIAS = new Map([['vscode', 'code']]); @@ -126,7 +126,7 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { const disposables = new DisposableStore(); const onSessionStoppedOrCanceled = (dispose: boolean) => { - this.activeVoiceChatSessions--; + this.activeVoiceChatSessions = Math.max(0, this.activeVoiceChatSessions - 1); if (this.activeVoiceChatSessions === 0) { this.voiceChatInProgress.reset(); } @@ -152,7 +152,8 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { disposables.add(session.onDidChange(e => { switch (e.status) { case SpeechToTextStatus.Recognizing: - case SpeechToTextStatus.Recognized: + case SpeechToTextStatus.Recognized: { + let massagedEvent: IVoiceChatTextEvent = e; if (e.text) { const startsWithAgent = e.text.startsWith(VoiceChatService.PHRASES_UPPER[VoiceChatService.AGENT_PREFIX]) || e.text.startsWith(VoiceChatService.PHRASES_LOWER[VoiceChatService.AGENT_PREFIX]); const startsWithSlashCommand = e.text.startsWith(VoiceChatService.PHRASES_UPPER[VoiceChatService.COMMAND_PREFIX]) || e.text.startsWith(VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]); @@ -208,15 +209,16 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { } } - emitter.fire({ + massagedEvent = { status: e.status, text: (transformedWords ?? originalWords).join(' '), waitingForInput - }); - - break; + }; } } + emitter.fire(massagedEvent); + break; + } case SpeechToTextStatus.Started: this.activeVoiceChatSessions++; this.voiceChatInProgress.set(true); @@ -226,7 +228,7 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { onSessionStoppedOrCanceled(false); emitter.fire(e); break; - default: + case SpeechToTextStatus.Error: emitter.fire(e); break; } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 86454907209..58586530c70 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -43,7 +43,7 @@ import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IVoiceChatService, VoiceChatInProgress as GlobalVoiceChatInProgress } from 'vs/workbench/contrib/chat/common/voiceChatService'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextInProgress, SpeechToTextStatus, TextToSpeechStatus, TextToSpeechInProgress as GlobalTextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -67,7 +67,7 @@ const TerminalChatExecute = MenuId.for('terminalChatInput'); // unfortunately, t // Global Context Keys (set on global context key service) const CanVoiceChat = ContextKeyExpr.and(CONTEXT_CHAT_ENABLED, HasSpeechProvider); const FocusInChatInput = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT); -const AnyChatRequestInProgress = ContextKeyExpr.or(CONTEXT_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, TerminalChatContextKeys.requestActive); +const AnyChatRequestInProgress = ContextKeyExpr.or(CONTEXT_CHAT_REQUEST_IN_PROGRESS, TerminalChatContextKeys.requestActive); // Scoped Context Keys (set on per-chat-context scoped context key service) const ScopedVoiceChatGettingReady = new RawContextKey('scopedVoiceChatGettingReady', false, { type: 'boolean', description: localize('scopedVoiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat. This key is only defined scoped, per chat context.") }); @@ -665,7 +665,10 @@ export class StopListeningAndSubmitAction extends Action2 { f1: true, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - when: FocusInChatInput, + when: ContextKeyExpr.and( + FocusInChatInput, + AnyScopedVoiceChatInProgress + ), primary: KeyMod.CtrlCmd | KeyCode.KeyI }, precondition: GlobalVoiceChatInProgress // need global context here because of `f1: true` @@ -802,7 +805,7 @@ class ChatSynthesizerSessions { let totalOffset = 0; let complete = false; do { - const responseLength = response.response.asString().length; + const responseLength = response.response.toString().length; const { chunk, offset } = this.parseNextChatResponseChunk(response, totalOffset); totalOffset = offset; complete = response.isComplete; @@ -815,7 +818,7 @@ class ChatSynthesizerSessions { return; } - if (!complete && responseLength === response.response.asString().length) { + if (!complete && responseLength === response.response.toString().length) { await raceCancellation(Event.toPromise(response.onDidChange), token); // wait for the response to change } } while (!token.isCancellationRequested && !complete); @@ -824,7 +827,7 @@ class ChatSynthesizerSessions { private parseNextChatResponseChunk(response: IChatResponseModel, offset: number): { readonly chunk: string | undefined; readonly offset: number } { let chunk: string | undefined = undefined; - const text = response.response.asString(); + const text = response.response.toString(); if (response.isComplete) { chunk = text.substring(offset); diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap index c1ba30be800..9def37d5acb 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap @@ -1 +1 @@ -
<!-- comment1 <div></div> --><div>content</div><!-- comment2 -->
\ No newline at end of file +
<!-- comment1 <div></div> -->
content
<!-- comment2 -->
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap index 02c52ac2aa4..9bfd3b945e8 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap @@ -1 +1 @@ -
1<canvas>2<div>3</div></canvas>4
\ No newline at end of file +

1<canvas>2</canvas>

<details>3</details>4

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap index 67381fee546..c0b5a277aac 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap @@ -1 +1 @@ -
1<div id="id1" style="display: none">2<div id="my id 2">3</div></div>4
\ No newline at end of file +

1

<details id="id1" style="display: none">2<details id="my id 2">3</details></details>4

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap index a58ce687e96..ba72307e533 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap @@ -1,8 +1,8 @@

heading

-<div> +<details>
    -
  • <div>1</div>
  • +
  • <details>1</details>
  • hi
-</div> +</details>
<canvas>canvas here</canvas>
<details></details>
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap index 247cce5ff8e..1241ef62b5f 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap @@ -1 +1 @@ -
<div><img src="http://disallowed.com/image.jpg"></div>
\ No newline at end of file +

<img src="http://disallowed.com/image.jpg">

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap index 023b2e6a846..5b482726d3a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap @@ -1 +1 @@ -
<area>

<input type="text" value="test">
\ No newline at end of file +

<area>



<input type="text" value="test">

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap new file mode 100644 index 00000000000..89991e7676e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap @@ -0,0 +1 @@ +

hello

\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap new file mode 100644 index 00000000000..d704b7b322d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap @@ -0,0 +1,4 @@ +
    +
  1. hello test text
  2. +
+
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts index f006d1afbf7..657ed1c961c 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts @@ -27,15 +27,27 @@ suite('ChatMarkdownRenderer', () => { await assertSnapshot(result.element.textContent); }); + test('supportHtml with one-line markdown', async () => { + const md = new MarkdownString('**hello**'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + + const md2 = new MarkdownString('1. [_hello_](https://example.com) test **text**'); + md2.supportHtml = true; + const result2 = store.add(testRenderer.render(md2)); + await assertSnapshot(result2.element.outerHTML); + }); + test('invalid HTML', async () => { - const md = new MarkdownString('12
3
4'); + const md = new MarkdownString('12
3
4'); md.supportHtml = true; const result = store.add(testRenderer.render(md)); await assertSnapshot(result.element.outerHTML); }); test('invalid HTML with attributes', async () => { - const md = new MarkdownString('14'); + const md = new MarkdownString('14'); md.supportHtml = true; const result = store.add(testRenderer.render(md)); await assertSnapshot(result.element.outerHTML); @@ -57,12 +69,12 @@ suite('ChatMarkdownRenderer', () => { test('mixed valid and invalid HTML', async () => { const md = new MarkdownString(`

heading

-
+
    -
  • 1
  • +
  • 1
  • hi
-
+
canvas here
`); md.supportHtml = true; const result = store.add(testRenderer.render(md)); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts index 40f72512a6a..2da25bc9817 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -19,6 +19,7 @@ import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVari import { MockChatWidgetService } from 'vs/workbench/contrib/chat/test/browser/mockChatWidget'; import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { TestViewsService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('ChatVariables', function () { @@ -28,7 +29,7 @@ suite('ChatVariables', function () { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); setup(function () { - service = new ChatVariablesService(new MockChatWidgetService()); + service = new ChatVariablesService(new MockChatWidgetService(), new TestViewsService()); instantiationService = testDisposables.add(new TestInstantiationService()); instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); instantiationService.stub(ILogService, new NullLogService()); diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index 383288306cb..78fe7781692 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -77,15 +77,7 @@ usedContext: { documents: [ { - uri: { - scheme: "file", - authority: "", - path: "/test/path/to/file", - query: "", - fragment: "", - _formatted: null, - _fsPath: null - }, + uri: URI(file:///test/path/to/file), version: 3, ranges: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index cf9cc42dfc6..be8bb0fed92 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -77,15 +77,7 @@ usedContext: { documents: [ { - uri: { - scheme: "file", - authority: "", - path: "/test/path/to/file", - query: "", - fragment: "", - _formatted: null, - _fsPath: null - }, + uri: URI(file:///test/path/to/file), version: 3, ranges: [ { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap index 5847d813dfe..b26d84334a0 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap @@ -9,15 +9,7 @@ kind: "markdownContent" }, { - inlineReference: { - scheme: "https", - authority: "microsoft.com", - path: "/", - query: "", - fragment: "", - _formatted: null, - _fsPath: null - }, + inlineReference: URI(https://microsoft.com/), kind: "inlineReference" }, { diff --git a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts new file mode 100644 index 00000000000..f14ffdaa651 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MockObject, mockObject } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ChatAgentService, IChatAgentData, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import assert from 'assert'; + +const testAgentId = 'testAgent'; +const testAgentData: IChatAgentData = { + id: testAgentId, + name: 'Test Agent', + extensionDisplayName: '', + extensionId: new ExtensionIdentifier(''), + extensionPublisherId: '', + locations: [], + metadata: {}, + slashCommands: [], +}; + +suite('ChatAgents', function () { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let chatAgentService: IChatAgentService; + let contextKeyService: MockObject; + setup(() => { + contextKeyService = mockObject()(); + chatAgentService = new ChatAgentService(contextKeyService as any); + }); + + test('registerAgent', async () => { + assert.strictEqual(chatAgentService.getAgents().length, 0); + + + const agentRegistration = chatAgentService.registerAgent(testAgentId, testAgentData); + + assert.strictEqual(chatAgentService.getAgents().length, 1); + assert.strictEqual(chatAgentService.getAgents()[0].id, testAgentId); + + assert.throws(() => chatAgentService.registerAgent(testAgentId, testAgentData)); + + agentRegistration.dispose(); + assert.strictEqual(chatAgentService.getAgents().length, 0); + }); + + test('agent when clause', async () => { + assert.strictEqual(chatAgentService.getAgents().length, 0); + + store.add(chatAgentService.registerAgent(testAgentId, { + ...testAgentData, + when: 'myKey' + })); + assert.strictEqual(chatAgentService.getAgents().length, 0); + + contextKeyService.contextMatchesRules.returns(true); + assert.strictEqual(chatAgentService.getAgents().length, 1); + }); + + suite('registerAgentImplementation', function () { + const agentImpl: IChatAgentImplementation = { + invoke: async () => { return {}; }, + provideFollowups: async () => { return []; }, + }; + + test('should register an agent implementation', () => { + store.add(chatAgentService.registerAgent(testAgentId, testAgentData)); + store.add(chatAgentService.registerAgentImplementation(testAgentId, agentImpl)); + + const agents = chatAgentService.getActivatedAgents(); + assert.strictEqual(agents.length, 1); + assert.strictEqual(agents[0].id, testAgentId); + }); + + test('can dispose an agent implementation', () => { + store.add(chatAgentService.registerAgent(testAgentId, testAgentData)); + const implRegistration = chatAgentService.registerAgentImplementation(testAgentId, agentImpl); + implRegistration.dispose(); + + const agents = chatAgentService.getActivatedAgents(); + assert.strictEqual(agents.length, 0); + }); + + test('should throw error if agent does not exist', () => { + assert.throws(() => chatAgentService.registerAgentImplementation('nonexistentAgent', agentImpl)); + }); + + test('should throw error if agent already has an implementation', () => { + store.add(chatAgentService.registerAgent(testAgentId, testAgentData)); + store.add(chatAgentService.registerAgentImplementation(testAgentId, agentImpl)); + + assert.throws(() => chatAgentService.registerAgentImplementation(testAgentId, agentImpl)); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index b9f38a04549..81efbdca4b6 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { URI } from 'vs/base/common/uri'; @@ -149,7 +149,7 @@ suite('ChatModel', () => { model2.acceptResponseProgress(request1, { content: new MarkdownString('Hello'), kind: 'markdownContent' }); - assert.strictEqual(request1.response.response.asString(), 'Hello'); + assert.strictEqual(request1.response.response.toString(), 'Hello'); }); }); @@ -162,7 +162,7 @@ suite('Response', () => { response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' }); await assertSnapshot(response.value); - assert.strictEqual(response.asString(), 'markdown1markdown2'); + assert.strictEqual(response.toString(), 'markdown1markdown2'); }); test('not mergeable markdown', async () => { diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 1e9ad196f8b..df32dfed2f4 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { assertSnapshot } from 'vs/base/test/common/snapshot'; @@ -127,7 +127,7 @@ suite('ChatService', () => { await testService.addCompleteRequest(model.sessionId, 'test request', undefined, 0, { message: 'test response' }); assert.strictEqual(model.getRequests().length, 1); assert.ok(model.getRequests()[0].response); - assert.strictEqual(model.getRequests()[0].response?.response.asString(), 'test response'); + assert.strictEqual(model.getRequests()[0].response?.response.toString(), 'test response'); }); test('sendRequest fails', async () => { diff --git a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts b/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts index 314b4589e31..665c2e386b1 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; @@ -13,31 +13,56 @@ suite('ChatWordCounter', () => { function doTest(str: string, nWords: number, resultStr: string) { const result = getNWords(str, nWords); assert.strictEqual(result.value, resultStr); - assert.strictEqual(result.actualWordCount, nWords); + assert.strictEqual(result.returnedWordCount, nWords); } - test('getNWords, matching actualWordCount', () => { - const cases: [string, number, string][] = [ - ['hello world', 1, 'hello'], - ['hello', 1, 'hello'], - ['hello world', 0, ''], - ['here\'s, some. punctuation?', 3, 'here\'s, some. punctuation?'], - ['| markdown | _table_ | header |', 3, '| markdown | _table_ | header'], - ['| --- | --- | --- |', 1, '| --- | --- | --- |'], - [' \t some \n whitespace \n\n\nhere ', 3, ' \t some \n whitespace \n\n\nhere'], - ]; + suite('getNWords', () => { + test('matching actualWordCount', () => { + const cases: [string, number, string][] = [ + ['hello world', 1, 'hello'], + ['hello', 1, 'hello'], + ['hello world', 0, ''], + ['here\'s, some. punctuation?', 3, 'here\'s, some. punctuation?'], + ['| markdown | _table_ | header |', 3, '| markdown | _table_ | header'], + ['| --- | --- | --- |', 1, '| ---'], + ['| --- | --- | --- |', 3, '| --- | --- | ---'], + [' \t some \n whitespace \n\n\nhere ', 3, ' \t some \n whitespace \n\n\nhere'], + ]; - cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + }); + + test('matching links', () => { + const cases: [string, number, string][] = [ + ['[hello](https://example.com) world', 1, '[hello](https://example.com)'], + ['[hello](https://example.com) world', 2, '[hello](https://example.com) world'], + ['oh [hello](https://example.com "title") world', 1, 'oh'], + ['oh [hello](https://example.com "title") world', 2, 'oh [hello](https://example.com "title")'], + ]; + + cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + }); + + test('code', () => { + const cases: [string, number, string][] = [ + ['let a=1-2', 2, 'let a'], + ['let a=1-2', 3, 'let a='], + ['let a=1-2', 4, 'let a=1'], + ['const myVar = 1+2', 4, 'const myVar = 1'], + ['
', 3, '
', 4, '
'], + ]; + + cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + }); + + test('chinese characters', () => { + const cases: [string, number, string][] = [ + ['我喜欢中国èœ', 3, '我喜欢'], + ]; + + cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + }); }); - test('getNWords, matching links', () => { - const cases: [string, number, string][] = [ - ['[hello](https://example.com) world', 1, '[hello](https://example.com)'], - ['[hello](https://example.com) world', 2, '[hello](https://example.com) world'], - ['oh [hello](https://example.com "title") world', 1, 'oh'], - ['oh [hello](https://example.com "title") world', 2, 'oh [hello](https://example.com "title")'], - ]; - - cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); - }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts new file mode 100644 index 00000000000..ebdde0c0d22 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { AsyncIterableSource, DeferredPromise, timeout } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { ChatMessageRole, IChatResponseFragment, languageModelExtensionPoint, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; + +suite('LanguageModels', function () { + + let languageModels: LanguageModelsService; + + const store = new DisposableStore(); + const activationEvents = new Set(); + + setup(function () { + + languageModels = new LanguageModelsService( + new class extends mock() { + override activateByEvent(name: string) { + activationEvents.add(name); + return Promise.resolve(); + } + }, + new NullLogService() + ); + + const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelExtensionPoint.name)!; + + ext.acceptUsers([{ + description: { ...nullExtensionDescription, enabledApiProposals: ['chatProvider'] }, + value: { vendor: 'test-vendor' }, + collector: null! + }]); + + + store.add(languageModels.registerLanguageModelChat('1', { + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Pretty Name', + vendor: 'test-vendor', + family: 'test-family', + version: 'test-version', + id: 'test-id', + maxInputTokens: 100, + maxOutputTokens: 100, + }, + sendChatRequest: async () => { + throw new Error(); + }, + provideTokenCount: async () => { + throw new Error(); + } + })); + + store.add(languageModels.registerLanguageModelChat('12', { + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Pretty Name', + vendor: 'test-vendor', + family: 'test2-family', + version: 'test2-version', + id: 'test-id', + maxInputTokens: 100, + maxOutputTokens: 100, + }, + sendChatRequest: async () => { + throw new Error(); + }, + provideTokenCount: async () => { + throw new Error(); + } + })); + }); + + teardown(function () { + languageModels.dispose(); + activationEvents.clear(); + store.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('empty selector returns all', async function () { + + const result1 = await languageModels.selectLanguageModels({}); + assert.deepStrictEqual(result1.length, 2); + assert.deepStrictEqual(result1[0], '1'); + assert.deepStrictEqual(result1[1], '12'); + }); + + test('no warning that a matching model was not found #213716', async function () { + const result1 = await languageModels.selectLanguageModels({ vendor: 'test-vendor' }); + assert.deepStrictEqual(result1.length, 2); + + const result2 = await languageModels.selectLanguageModels({ vendor: 'test-vendor', family: 'FAKE' }); + assert.deepStrictEqual(result2.length, 0); + }); + + test('sendChatRequest returns a response-stream', async function () { + + store.add(languageModels.registerLanguageModelChat('actual', { + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Pretty Name', + vendor: 'test-vendor', + family: 'actual-family', + version: 'actual-version', + id: 'actual-lm', + maxInputTokens: 100, + maxOutputTokens: 100, + }, + sendChatRequest: async (messages, _from, _options, token) => { + // const message = messages.at(-1); + + const defer = new DeferredPromise(); + const stream = new AsyncIterableSource(); + + (async () => { + while (!token.isCancellationRequested) { + stream.emitOne({ index: 0, part: { type: 'text', value: Date.now().toString() } }); + await timeout(10); + } + defer.complete(undefined); + })(); + + return { + stream: stream.asyncIterable, + result: defer.p + }; + }, + provideTokenCount: async () => { + throw new Error(); + } + })); + + const models = await languageModels.selectLanguageModels({ identifier: 'actual-lm' }); + assert.ok(models.length === 1); + + const first = models[0]; + + const cts = new CancellationTokenSource(); + + const request = await languageModels.sendChatRequest(first, nullExtensionDescription.identifier, [{ role: ChatMessageRole.User, content: { type: 'text', value: 'hello' } }], {}, cts.token); + + assert.ok(request); + + cts.dispose(true); + + await request.result; + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index 2ff31b0173c..2ad2f91c6b1 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -70,6 +70,7 @@ suite('VoiceChat', () => { getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); } registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise): IDisposable { throw new Error('Method not implemented.'); } getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } + agentHasDupeName(id: string): boolean { throw new Error('Method not implemented.'); } } class TestSpeechService implements ISpeechService { diff --git a/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts b/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts index 249fd8e457c..85d4a9cdb2f 100644 --- a/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts +++ b/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { parseNextChatResponseChunk } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; diff --git a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts index 38fd5502ae2..17e6217a6a9 100644 --- a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts +++ b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts @@ -8,6 +8,7 @@ import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { Disposable } from 'vs/base/common/lifecycle'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { codeActionCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/common/types'; import * as nls from 'vs/nls'; @@ -34,15 +35,11 @@ const createCodeActionsAutoSave = (description: string): IJSONSchema => { }; }; -const codeActionsOnSaveDefaultProperties = Object.freeze({ - 'source.fixAll': createCodeActionsAutoSave(nls.localize('codeActionsOnSave.fixAll', "Controls whether auto fix action should be run on file save.")), -}); const codeActionsOnSaveSchema: IConfigurationPropertySchema = { oneOf: [ { type: 'object', - properties: codeActionsOnSaveDefaultProperties, additionalProperties: { type: 'string' }, @@ -72,15 +69,24 @@ export const editorConfiguration = Object.freeze({ export class CodeActionsContribution extends Disposable implements IWorkbenchContribution { private _contributedCodeActions: CodeActionsExtensionPoint[] = []; + private settings: Set = new Set(); private readonly _onDidChangeContributions = this._register(new Emitter()); constructor( codeActionsExtensionPoint: IExtensionPoint, @IKeybindingService keybindingService: IKeybindingService, + @ILanguageFeaturesService private readonly languageFeatures: ILanguageFeaturesService ) { super(); + // TODO: @justschen caching of code actions based on extensions loaded: https://github.com/microsoft/vscode/issues/216019 + + languageFeatures.codeActionProvider.onDidChange(() => { + this.updateSettingsFromCodeActionProviders(); + this.updateConfigurationSchemaFromContribs(); + }, 2000); + codeActionsExtensionPoint.setHandler(extensionPoints => { this._contributedCodeActions = extensionPoints.flatMap(x => x.value).filter(x => Array.isArray(x.actions)); this.updateConfigurationSchema(this._contributedCodeActions); @@ -93,9 +99,23 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon }); } + private updateSettingsFromCodeActionProviders(): void { + const providers = this.languageFeatures.codeActionProvider.allNoModel(); + providers.forEach(provider => { + if (provider.providedCodeActionKinds) { + provider.providedCodeActionKinds.forEach(kind => { + if (!this.settings.has(kind) && CodeActionKind.Source.contains(new HierarchicalKind(kind))) { + this.settings.add(kind); + } + }); + } + }); + } + private updateConfigurationSchema(codeActionContributions: readonly CodeActionsExtensionPoint[]) { - const newProperties: IJSONSchemaMap = { ...codeActionsOnSaveDefaultProperties }; + const newProperties: IJSONSchemaMap = {}; for (const [sourceAction, props] of this.getSourceActions(codeActionContributions)) { + this.settings.add(sourceAction); newProperties[sourceAction] = createCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", props.title)); } codeActionsOnSaveSchema.properties = newProperties; @@ -103,16 +123,24 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon .notifyConfigurationSchemaUpdated(editorConfiguration); } + private updateConfigurationSchemaFromContribs() { + const properties: IJSONSchemaMap = { ...codeActionsOnSaveSchema.properties }; + for (const codeActionKind of this.settings) { + if (!properties[codeActionKind]) { + properties[codeActionKind] = createCodeActionsAutoSave(nls.localize('codeActionsOnSave.generic', "Controls whether '{0}' actions should be run on file save.", codeActionKind)); + } + } + codeActionsOnSaveSchema.properties = properties; + Registry.as(Extensions.Configuration) + .notifyConfigurationSchemaUpdated(editorConfiguration); + } + private getSourceActions(contributions: readonly CodeActionsExtensionPoint[]) { - const defaultKinds = Object.keys(codeActionsOnSaveDefaultProperties).map(value => new HierarchicalKind(value)); const sourceActions = new Map(); for (const contribution of contributions) { for (const action of contribution.actions) { const kind = new HierarchicalKind(action.kind); - if (CodeActionKind.Source.contains(kind) - // Exclude any we already included by default - && !defaultKinds.some(defaultKind => defaultKind.contains(kind)) - ) { + if (CodeActionKind.Source.contains(kind)) { sourceActions.set(kind.value, action); } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts index 705308f8619..bfef8956ae8 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts @@ -29,6 +29,7 @@ import { assertIsDefined } from 'vs/base/common/types'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { toAction } from 'vs/base/common/actions'; import { ThemeIcon } from 'vs/base/common/themables'; +import { isWindows } from 'vs/base/common/platform'; const EDITOR_DICTATION_IN_PROGRESS = new RawContextKey('editorDictation.inProgress', false); const VOICE_CATEGORY = localize2('voiceCategory', "Voice"); @@ -48,7 +49,10 @@ export class EditorDictationStartAction extends EditorAction2 { f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyV, - weight: KeybindingWeight.WorkbenchContrib + weight: KeybindingWeight.WorkbenchContrib, + secondary: isWindows ? [ + KeyMod.Alt | KeyCode.Backquote + ] : undefined } }); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts index 6ca47252ca5..ef4ce7693a1 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts @@ -61,4 +61,5 @@ export class DiffEditorAccessibilityHelp implements IAccessibleViewImplentation options: { type: AccessibleViewType.Help } }; } + dispose() { } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 052608edd82..15fba768221 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -33,7 +33,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont const isEmbeddedDiffEditor = this._diffEditor instanceof EmbeddedDiffEditorWidget; if (!isEmbeddedDiffEditor) { - const computationResult = observableFromEvent(e => this._diffEditor.onDidUpdateDiff(e), () => /** @description diffEditor.diffComputationResult */ this._diffEditor.getDiffComputationResult()); + const computationResult = observableFromEvent(this, e => this._diffEditor.onDidUpdateDiff(e), () => /** @description diffEditor.diffComputationResult */ this._diffEditor.getDiffComputationResult()); const onlyWhiteSpaceChange = computationResult.map(r => r && !r.identical && r.changes2.length === 0); this._register(autorunWithStore((reader, store) => { diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index b047830a881..952a32eac79 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -29,33 +29,16 @@ import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLa import { OS } from 'vs/base/common/platform'; import { status } from 'vs/base/browser/ui/aria/aria'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; import { LOG_MODE_ID, OUTPUT_MODE_ID } from 'vs/workbench/services/output/common/output'; import { SEARCH_RESULT_LANGUAGE_ID } from 'vs/workbench/services/search/common/search'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { ChatAgentLocation, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; const $ = dom.$; -// TODO@joyceerhl remove this after a few iterations -Registry.as(Extensions.ConfigurationMigration) - .registerConfigurationMigrations([{ - key: 'workbench.editor.untitled.hint', - migrateFn: (value, _accessor) => ([ - [emptyTextEditorHintSetting, { value }], - ['workbench.editor.untitled.hint', { value: undefined }] - ]) - }, - { - key: 'accessibility.verbosity.untitledHint', - migrateFn: (value, _accessor) => ([ - [AccessibilityVerbositySettingId.EmptyEditorHint, { value }], - ['accessibility.verbosity.untitledHint', { value: undefined }] - ]) - }]); - export interface IEmptyTextEditorHintOptions { readonly clickable?: boolean; } @@ -79,6 +62,7 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { @IChatAgentService private readonly chatAgentService: IChatAgentService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService protected readonly productService: IProductService, + @IContextMenuService private readonly contextMenuService: IContextMenuService ) { this.toDispose = []; this.toDispose.push(this.editor.onDidChangeModel(() => this.update())); @@ -147,7 +131,7 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { } const hasEditorAgents = Boolean(this.chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); - const shouldRenderDefaultHint = model?.uri.scheme === Schemas.untitled && languageId === PLAINTEXT_LANGUAGE_ID && hasEditorAgents; + const shouldRenderDefaultHint = model?.uri.scheme === Schemas.untitled && languageId === PLAINTEXT_LANGUAGE_ID; return hasEditorAgents || shouldRenderDefaultHint; } @@ -164,7 +148,8 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { this.keybindingService, this.chatAgentService, this.telemetryService, - this.productService + this.productService, + this.contextMenuService ); } else if (!shouldRenderHint && this.textHintContentWidget) { this.textHintContentWidget.dispose(); @@ -197,7 +182,8 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { private readonly keybindingService: IKeybindingService, private readonly chatAgentService: IChatAgentService, private readonly telemetryService: ITelemetryService, - private readonly productService: IProductService + private readonly productService: IProductService, + private readonly contextMenuService: IContextMenuService, ) { this.toDispose = new DisposableStore(); this.toDispose.add(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { @@ -218,6 +204,36 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { return EmptyTextEditorHintContentWidget.ID; } + private _disableHint(e?: MouseEvent) { + const disableHint = () => { + this.configurationService.updateValue(emptyTextEditorHintSetting, 'hidden'); + this.dispose(); + this.editor.focus(); + }; + + if (!e) { + disableHint(); + return; + } + + this.contextMenuService.showContextMenu({ + getAnchor: () => { return new StandardMouseEvent(dom.getActiveWindow(), e); }, + getActions: () => { + return [{ + id: 'workench.action.disableEmptyEditorHint', + label: localize('disableEditorEmptyHint', "Disable Empty Editor Hint"), + tooltip: localize('disableEditorEmptyHint', "Disable Empty Editor Hint"), + enabled: true, + class: undefined, + run: () => { + disableHint(); + } + } + ]; + } + }); + } + private _getHintInlineChat(providers: IChatAgent[]) { const providerName = (providers.length === 1 ? providers[0].fullName : undefined) ?? this.productService.nameShort; @@ -257,6 +273,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { const hintPart = $('a', undefined, fragment); hintPart.style.fontStyle = 'italic'; hintPart.style.cursor = 'pointer'; + this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CONTEXT_MENU, (e) => this._disableHint(e))); this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CLICK, handleClick)); return hintPart; } else { @@ -275,6 +292,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { if (this.options.clickable) { label.element.style.cursor = 'pointer'; + this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CONTEXT_MENU, (e) => this._disableHint(e))); this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CLICK, handleClick)); } @@ -297,7 +315,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { hintElement.appendChild(rendered); } - return { ariaLabel, hintHandler, hintElement }; + return { ariaLabel, hintElement }; } private _getHintDefault() { @@ -315,7 +333,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { chooseEditorOnClickOrTap(event.browserEvent); break; case '3': - dontShowOnClickOrTap(); + this._disableHint(); break; } } @@ -330,7 +348,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { id: ChangeLanguageAction.ID, from: 'hint' }); - await this.commandService.executeCommand(ChangeLanguageAction.ID, { from: 'hint' }); + await this.commandService.executeCommand(ChangeLanguageAction.ID); this.editor.focus(); }; @@ -360,12 +378,6 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { } }; - const dontShowOnClickOrTap = () => { - this.configurationService.updateValue(emptyTextEditorHintSetting, 'hidden'); - this.dispose(); - this.editor.focus(); - }; - const hintMsg = localize({ key: 'message', comment: [ @@ -387,7 +399,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { anchor.style.cursor = 'pointer'; const id = keybindingsLookup.shift(); const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel(); - hintHandler.disposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), anchor, title ?? '')); + hintHandler.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), anchor, title ?? '')); } return { hintElement, ariaLabel }; diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index bfbdc1d3f50..c7390207969 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -278,9 +278,7 @@ export abstract class SimpleFindWidget extends Widget implements IVerticalSashLa override dispose() { super.dispose(); - if (this._domNode && this._domNode.parentElement) { - this._domNode.parentElement.removeChild(this._domNode); - } + this._domNode?.remove(); } public isVisible(): boolean { diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index e4d481e7fe1..8b2fa1d6632 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -326,7 +326,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { if (semanticTokenInfo.metadata[property] !== undefined) { const definition = semanticTokenInfo.definitions[property]; const defValue = this._renderTokenStyleDefinition(definition, property); - const defValueStr = defValue.map(el => el instanceof HTMLElement ? el.outerHTML : el).join(); + const defValueStr = defValue.map(el => dom.isHTMLElement(el) ? el.outerHTML : el).join(); let properties = propertiesByDefValue[defValueStr]; if (!properties) { propertiesByDefValue[defValueStr] = properties = []; diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 71a1b593698..2a40f9d9413 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -118,7 +118,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess return []; } - return this.doGetSymbolPicks(this.getDocumentSymbols(model, token), prepareQuery(filter), options, token); + return this.doGetSymbolPicks(this.getDocumentSymbols(model, token), prepareQuery(filter), options, token, model); } //#endregion diff --git a/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts index b4f68265026..f7660482b85 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts @@ -25,7 +25,8 @@ export function getSimpleEditorOptions(configurationService: IConfigurationServi hideCursorInOverviewRuler: true, selectionHighlight: false, scrollbar: { - horizontal: 'hidden' + horizontal: 'hidden', + alwaysConsumeMouseWheel: false }, lineDecorationsWidth: 0, overviewRulerBorder: false, diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 6fbc04ff214..e06219499e2 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -162,7 +162,7 @@ export class SuggestEnabledInput extends Widget { const scopedContextKeyService = this.getScopedContextKeyService(contextKeyService); const instantiationService = scopedContextKeyService - ? defaultInstantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])) + ? this._register(defaultInstantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService]))) : defaultInstantiationService; this.inputWidget = this._register(instantiationService.createInstance(CodeEditorWidget, this.stylingContainer, diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts index f5b2c84ebcb..5b5081623cf 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize, localize2 } from 'vs/nls'; +import { Disposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; +import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; export class ToggleMultiCursorModifierAction extends Action2 { @@ -39,7 +40,7 @@ export class ToggleMultiCursorModifierAction extends Action2 { const multiCursorModifier = new RawContextKey('multiCursorModifier', 'altKey'); -class MultiCursorModifierContextKeyController implements IWorkbenchContribution { +class MultiCursorModifierContextKeyController extends Disposable implements IWorkbenchContribution { private readonly _multiCursorModifier: IContextKey; @@ -47,14 +48,15 @@ class MultiCursorModifierContextKeyController implements IWorkbenchContribution @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService ) { + super(); this._multiCursorModifier = multiCursorModifier.bindTo(contextKeyService); this._update(); - configurationService.onDidChangeConfiguration((e) => { + this._register(configurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('editor.multiCursorModifier')) { this._update(); } - }); + })); } private _update(): void { diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts index 7cdd7d910ad..043e3f5c9e7 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts @@ -3,25 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { addDisposableListener, onDidRegisterWindow } from 'vs/base/browser/dom'; +import { mainWindow } from 'vs/base/browser/window'; +import { Codicon } from 'vs/base/common/codicons'; +import { Event } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor, ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution, registerDiffEditorContribution, EditorContributionInstantiation } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, EditorContributionInstantiation, ServicesAccessor, registerDiffEditorContribution, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IDiffEditorContribution, IEditorContribution } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; +import * as nls from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { Codicon } from 'vs/base/common/codicons'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { Event } from 'vs/base/common/event'; -import { addDisposableListener, onDidRegisterWindow } from 'vs/base/browser/dom'; -import { mainWindow } from 'vs/base/browser/window'; const transientWordWrapState = 'transientWordWrapState'; const isWordWrapMinifiedKey = 'isWordWrapMinified'; @@ -271,7 +271,7 @@ class EditorWordWrapContextKeyTracker extends Disposable implements IWorkbenchCo disposables.add(addDisposableListener(window, 'focus', () => this._update(), true)); disposables.add(addDisposableListener(window, 'blur', () => this._update(), true)); }, { window: mainWindow, disposables: this._store })); - this._editorService.onDidActiveEditorChange(() => this._update()); + this._register(this._editorService.onDidActiveEditorChange(() => this._update())); this._canToggleWordWrap = CAN_TOGGLE_WORD_WRAP.bindTo(this._contextService); this._editorWordWrap = EDITOR_WORD_WRAP.bindTo(this._contextService); this._activeEditor = null; diff --git a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index f8960304793..42cc4253ad2 100644 --- a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FinalNewLineParticipant, TrimFinalNewLinesParticipant, TrimWhitespaceParticipant } from 'vs/workbench/contrib/codeEditor/browser/saveParticipants'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index 9344f1a46a6..fe3632fb8d0 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; @@ -22,6 +22,7 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { EncodedTokenizationResult, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages'; import { NullState } from 'vs/editor/common/languages/nullTokenize'; import { MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { ITextModel } from 'vs/editor/common/model'; function getIRange(range: IRange): IRange { return { @@ -36,12 +37,22 @@ const enum LanguageId { TypeScript = 'ts-test' } -function registerLanguage(instantiationService: TestInstantiationService, languageId: LanguageId): IDisposable { - const languageService = instantiationService.get(ILanguageService) - return languageService.registerLanguage({ id: languageId }); +function forceTokenizationFromLineToLine(model: ITextModel, startLine: number, endLine: number): void { + for (let line = startLine; line <= endLine; line++) { + model.tokenization.forceTokenization(line); + } } -function registerLanguageConfiguration(languageConfigurationService: ILanguageConfigurationService, languageId: LanguageId): IDisposable { +function registerLanguage(instantiationService: TestInstantiationService, languageId: LanguageId): IDisposable { + const disposables = new DisposableStore(); + const languageService = instantiationService.get(ILanguageService); + disposables.add(registerLanguageConfiguration(instantiationService, languageId)); + disposables.add(languageService.registerLanguage({ id: languageId })); + return disposables; +} + +function registerLanguageConfiguration(instantiationService: TestInstantiationService, languageId: LanguageId): IDisposable { + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); let configPath: string; switch (languageId) { case LanguageId.TypeScript: @@ -61,7 +72,7 @@ interface StandardTokenTypeData { standardTokenType: StandardTokenType; } -function registerTokenizationSupport(instantiationService: TestInstantiationService, tokens: StandardTokenTypeData[][], languageId: string): IDisposable { +function registerTokenizationSupport(instantiationService: TestInstantiationService, tokens: StandardTokenTypeData[][], languageId: LanguageId): IDisposable { let lineIndex = 0; const languageService = instantiationService.get(ILanguageService); const tokenizationSupport: ITokenizationSupport = { @@ -97,7 +108,7 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { languageConfigurationService = instantiationService.get(ILanguageConfigurationService); disposables.add(instantiationService); disposables.add(registerLanguage(instantiationService, languageId)); - disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); + disposables.add(registerLanguageConfiguration(instantiationService, languageId)); }); teardown(() => { @@ -218,8 +229,7 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { ]; disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); - model.tokenization.forceTokenization(1); - model.tokenization.forceTokenization(2); + forceTokenizationFromLineToLine(model, 1, 2); const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); assert.deepStrictEqual(editOperations.length, 1); const operation = editOperations[0]; @@ -338,8 +348,7 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { ]; disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); - model.tokenization.forceTokenization(1); - model.tokenization.forceTokenization(2); + forceTokenizationFromLineToLine(model, 1, 2); const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); assert.deepStrictEqual(editOperations.length, 1); const operation = editOperations[0]; @@ -373,6 +382,38 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { assert.deepStrictEqual(editOperations.length, 0); }); + test('Issue #209859: do not do reindentation for tokens inside of a string', () => { + + // issue: https://github.com/microsoft/vscode/issues/209859 + + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 12, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.String }, + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.String }, + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); + const fileContents = [ + 'const foo = `some text', + ' which is strangely', + ' indented. It should', + ' not be reindented.`' + ].join('\n'); + const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + forceTokenizationFromLineToLine(model, 1, 4); + const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); + assert.deepStrictEqual(editOperations.length, 0); + }); + // Failing tests inferred from the current regexes... test.skip('Incorrect deindentation after `*/}` string', () => { diff --git a/src/vs/workbench/contrib/comments/browser/commentColors.ts b/src/vs/workbench/contrib/comments/browser/commentColors.ts index 08d44a3224b..b66b9590f76 100644 --- a/src/vs/workbench/contrib/comments/browser/commentColors.ts +++ b/src/vs/workbench/contrib/comments/browser/commentColors.ts @@ -13,11 +13,11 @@ import { IColorTheme } from 'vs/platform/theme/common/themeService'; const resolvedCommentViewIcon = registerColor('commentsView.resolvedIcon', { dark: disabledForeground, light: disabledForeground, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('resolvedCommentIcon', 'Icon color for resolved comments.')); const unresolvedCommentViewIcon = registerColor('commentsView.unresolvedIcon', { dark: listFocusOutline, light: listFocusOutline, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentIcon', 'Icon color for unresolved comments.')); -registerColor('editorCommentsWidget.replyInputBackground', { dark: peekViewTitleBackground, light: peekViewTitleBackground, hcDark: peekViewTitleBackground, hcLight: peekViewTitleBackground }, nls.localize('commentReplyInputBackground', 'Background color for comment reply input box.')); +registerColor('editorCommentsWidget.replyInputBackground', peekViewTitleBackground, nls.localize('commentReplyInputBackground', 'Background color for comment reply input box.')); const resolvedCommentBorder = registerColor('editorCommentsWidget.resolvedBorder', { dark: resolvedCommentViewIcon, light: resolvedCommentViewIcon, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('resolvedCommentBorder', 'Color of borders and arrow for resolved comments.')); const unresolvedCommentBorder = registerColor('editorCommentsWidget.unresolvedBorder', { dark: unresolvedCommentViewIcon, light: unresolvedCommentViewIcon, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentBorder', 'Color of borders and arrow for unresolved comments.')); -export const commentThreadRangeBackground = registerColor('editorCommentsWidget.rangeBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); -export const commentThreadRangeActiveBackground = registerColor('editorCommentsWidget.rangeActiveBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadActiveRangeBackground', 'Color of background for currently selected or hovered comment range.')); +export const commentThreadRangeBackground = registerColor('editorCommentsWidget.rangeBackground', transparent(unresolvedCommentBorder, .1), nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); +export const commentThreadRangeActiveBackground = registerColor('editorCommentsWidget.rangeActiveBackground', transparent(unresolvedCommentBorder, .1), nls.localize('commentThreadActiveRangeBackground', 'Color of background for currently selected or hovered comment range.')); const commentThreadStateBorderColors = new Map([ [languages.CommentThreadState.Unresolved, unresolvedCommentBorder], diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 92b52ac5402..725b522f956 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -14,11 +14,11 @@ import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { CommentThreadState } from 'vs/editor/common/languages'; export const overviewRulerCommentingRangeForeground = registerColor('editorGutter.commentRangeForeground', { dark: opaque(listInactiveSelectionBackground, editorBackground), light: darken(opaque(listInactiveSelectionBackground, editorBackground), .05), hcDark: Color.white, hcLight: Color.black }, nls.localize('editorGutterCommentRangeForeground', 'Editor gutter decoration color for commenting ranges. This color should be opaque.')); -const overviewRulerCommentForeground = registerColor('editorOverviewRuler.commentForeground', { dark: overviewRulerCommentingRangeForeground, light: overviewRulerCommentingRangeForeground, hcDark: overviewRulerCommentingRangeForeground, hcLight: overviewRulerCommentingRangeForeground }, nls.localize('editorOverviewRuler.commentForeground', 'Editor overview ruler decoration color for resolved comments. This color should be opaque.')); -const overviewRulerCommentUnresolvedForeground = registerColor('editorOverviewRuler.commentUnresolvedForeground', { dark: overviewRulerCommentForeground, light: overviewRulerCommentForeground, hcDark: overviewRulerCommentForeground, hcLight: overviewRulerCommentForeground }, nls.localize('editorOverviewRuler.commentUnresolvedForeground', 'Editor overview ruler decoration color for unresolved comments. This color should be opaque.')); +const overviewRulerCommentForeground = registerColor('editorOverviewRuler.commentForeground', overviewRulerCommentingRangeForeground, nls.localize('editorOverviewRuler.commentForeground', 'Editor overview ruler decoration color for resolved comments. This color should be opaque.')); +const overviewRulerCommentUnresolvedForeground = registerColor('editorOverviewRuler.commentUnresolvedForeground', overviewRulerCommentForeground, nls.localize('editorOverviewRuler.commentUnresolvedForeground', 'Editor overview ruler decoration color for unresolved comments. This color should be opaque.')); const editorGutterCommentGlyphForeground = registerColor('editorGutter.commentGlyphForeground', { dark: editorForeground, light: editorForeground, hcDark: Color.black, hcLight: Color.white }, nls.localize('editorGutterCommentGlyphForeground', 'Editor gutter decoration color for commenting glyphs.')); -registerColor('editorGutter.commentUnresolvedGlyphForeground', { dark: editorGutterCommentGlyphForeground, light: editorGutterCommentGlyphForeground, hcDark: editorGutterCommentGlyphForeground, hcLight: editorGutterCommentGlyphForeground }, nls.localize('editorGutterCommentUnresolvedGlyphForeground', 'Editor gutter decoration color for commenting glyphs for unresolved comment threads.')); +registerColor('editorGutter.commentUnresolvedGlyphForeground', editorGutterCommentGlyphForeground, nls.localize('editorGutterCommentUnresolvedGlyphForeground', 'Editor gutter decoration color for commenting glyphs for unresolved comment threads.')); export class CommentGlyphWidget { public static description = 'comment-glyph-widget'; diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 47e5285466c..f7f6688cfbe 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -122,7 +122,7 @@ export class CommentNode extends Disposable { super(); this._domNode = dom.$('div.review-comment'); - this._contextKeyService = contextKeyService.createScoped(this._domNode); + this._contextKeyService = this._register(contextKeyService.createScoped(this._domNode)); this._commentContextValue = CommentContextKeys.commentContext.bindTo(this._contextKeyService); if (this.comment.contextValue) { this._commentContextValue.set(this.comment.contextValue); diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 3580e96799b..0a73f27023a 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -22,8 +22,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; @@ -63,7 +61,6 @@ export class CommentReply extends Disposable { focus: boolean, private _actionRunDelegate: (() => void) | null, @ICommentService private commentService: ICommentService, - @IThemeService private themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, @IKeybindingService private keybindingService: IKeybindingService, @IHoverService private hoverService: IHoverService, @@ -117,9 +114,11 @@ export class CommentReply extends Disposable { this.setCommentEditorDecorations(); // Only add the additional step of clicking a reply button to expand the textarea when there are existing comments - if (hasExistingComments) { + if (this._pendingComment) { + this.expandReplyArea(); + } else if (hasExistingComments) { this.createReplyButton(this.commentEditor, this.form); - } else if (focus && ((this._commentThread.comments && this._commentThread.comments.length === 0) || this._pendingComment)) { + } else if (focus && (!this._commentThread.comments || this._commentThread.comments.length === 0)) { this.expandReplyArea(); } this._error = dom.append(this.form, dom.$('.validation-error.hidden')); @@ -211,32 +210,12 @@ export class CommentReply extends Disposable { } setCommentEditorDecorations() { - const model = this.commentEditor.getModel(); - if (model) { - const valueLength = model.getValueLength(); - const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; - const placeholder = valueLength > 0 - ? '' - : hasExistingComments - ? (this._commentOptions?.placeHolder || nls.localize('reply', "Reply...")) - : (this._commentOptions?.placeHolder || nls.localize('newComment', "Type a new comment")); - const decorations = [{ - range: { - startLineNumber: 0, - endLineNumber: 0, - startColumn: 0, - endColumn: 1 - }, - renderOptions: { - after: { - contentText: placeholder, - color: `${resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4)}` - } - } - }]; + const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; + const placeholder = hasExistingComments + ? (this._commentOptions?.placeHolder || nls.localize('reply', "Reply...")) + : (this._commentOptions?.placeHolder || nls.localize('newComment', "Type a new comment")); - this.commentEditor.setDecorationsByType('review-zone-widget', COMMENTEDITOR_DECORATION_KEY, decorations); - } + this.commentEditor.updateOptions({ placeholder }); } private createTextModelListener(commentEditor: ICodeEditor, commentForm: HTMLElement) { @@ -366,7 +345,7 @@ export class CommentReply extends Disposable { private createReplyButton(commentEditor: ICodeEditor, commentForm: HTMLElement) { this._reviewThreadReplyButton = dom.append(commentForm, dom.$(`button.review-thread-reply-button.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this._reviewThreadReplyButton, this._commentOptions?.prompt || nls.localize('reply', "Reply..."))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this._reviewThreadReplyButton, this._commentOptions?.prompt || nls.localize('reply', "Reply..."))); this._reviewThreadReplyButton.textContent = this._commentOptions?.prompt || nls.localize('reply', "Reply..."); // bind click/escape actions for reviewThreadReplyButton and textArea diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts index 14db2828ee2..7156f465fa5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import * as nls from 'vs/nls'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore } from 'vs/base/common/lifecycle'; import * as languages from 'vs/editor/common/languages'; import { Emitter } from 'vs/base/common/event'; import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; @@ -30,7 +30,7 @@ export class CommentThreadBody extends D private _onDidResize = new Emitter(); onDidResize = this._onDidResize.event; - private _commentDisposable = new Map, IDisposable>(); + private _commentDisposable = new DisposableMap, DisposableStore>(); private _markdownRenderer: MarkdownRenderer; get length() { @@ -90,6 +90,7 @@ export class CommentThreadBody extends D } })); + this._commentDisposable.clearAndDisposeAll(); this._commentElements = []; if (this._commentThread.comments) { for (const comment of this._commentThread.comments) { @@ -177,11 +178,10 @@ export class CommentThreadBody extends D // del removed elements for (let i = commentElementsToDel.length - 1; i >= 0; i--) { const commentToDelete = commentElementsToDel[i]; - this._commentDisposable.get(commentToDelete)?.dispose(); - this._commentDisposable.delete(commentToDelete); + this._commentDisposable.deleteAndDispose(commentToDelete); this._commentElements.splice(commentElementsToDelIndex[i], 1); - this._commentsElement.removeChild(commentToDelete.domNode); + commentToDelete.domNode.remove(); } @@ -267,10 +267,12 @@ export class CommentThreadBody extends D this._parentCommentThreadWidget, this._markdownRenderer) as unknown as CommentNode; - this._register(newCommentNode); - this._commentDisposable.set(newCommentNode, newCommentNode.onDidClick(clickedNode => + const disposables: DisposableStore = new DisposableStore(); + disposables.add(newCommentNode.onDidClick(clickedNode => this._setFocusedComment(this._commentElements.findIndex(commentNode => commentNode.comment.uniqueIdInThread === clickedNode.comment.uniqueIdInThread)) )); + disposables.add(newCommentNode); + this._commentDisposable.set(newCommentNode, disposables); return newCommentNode; } @@ -283,6 +285,6 @@ export class CommentThreadBody extends D this._resizeObserver = null; } - this._commentDisposable.forEach(v => v.dispose()); + this._commentDisposable.dispose(); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts index 9784625cd2f..8333654958e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, ActionRunner } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import * as languages from 'vs/editor/common/languages'; import { IRange } from 'vs/editor/common/core/range'; @@ -46,6 +46,7 @@ export class CommentThreadHeader extends Disposable { super(); this._headElement = dom.$('.head'); container.appendChild(this._headElement); + this._register(toDisposable(() => this._headElement.remove())); this._fillHead(); } @@ -65,6 +66,7 @@ export class CommentThreadHeader extends Disposable { this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => this._delegate.collapse()); const menu = this._commentMenus.getCommentThreadTitleActions(this._contextKeyService); + this._register(menu); this.setActionBarActions(menu); this._register(menu); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 3c61b8bc26f..521455bb77a 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/review'; import * as dom from 'vs/base/browser/dom'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import * as languages from 'vs/editor/common/languages'; import { IMarkdownRendererOptions } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; @@ -88,7 +88,7 @@ export class CommentThreadWidget extends this._commentMenus = this.commentService.getCommentMenus(this._owner); - this._header = new CommentThreadHeader( + this._register(this._header = new CommentThreadHeader( container, { collapse: this.collapse.bind(this) @@ -98,12 +98,13 @@ export class CommentThreadWidget extends this._contextKeyService, this._scopedInstantiationService, contextMenuService - ); + )); this._header.updateCommentThread(this._commentThread); const bodyElement = dom.$('.body'); container.appendChild(bodyElement); + this._register(toDisposable(() => bodyElement.remove())); const tracker = this._register(dom.trackFocus(bodyElement)); this._register(registerNavigableContainer({ diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 7a6edd60c6d..2c909a2ed5e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -139,9 +139,9 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget super(editor, { keepEditorSelection: true, isAccessible: true }); this._contextKeyService = contextKeyService.createScoped(this.domNode); - this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection( + this._scopedInstantiationService = this._globalToDispose.add(instantiationService.createChild(new ServiceCollection( [IContextKeyService, this._contextKeyService] - )); + ))); const controller = this.commentService.getCommentController(this._uniqueOwner); if (controller) { @@ -150,7 +150,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._initialCollapsibleState = _pendingComment ? languages.CommentThreadCollapsibleState.Expanded : _commentThread.initialCollapsibleState; _commentThread.initialCollapsibleState = this._initialCollapsibleState; - this._isExpanded = this._initialCollapsibleState === languages.CommentThreadCollapsibleState.Expanded; this._commentThreadDisposables = []; this.create(); @@ -439,7 +438,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } _refresh(dimensions: dom.Dimension) { - if (dimensions.height === 0 && dimensions.width === 0) { + if ((this._isExpanded === undefined) && (dimensions.height === 0) && (dimensions.width === 0)) { this.commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; return; } diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts index 5ce9ac9a0ba..1c247dec349 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts @@ -48,4 +48,5 @@ export class CommentsAccessibilityHelp implements IAccessibleViewImplentation { getProvider(accessor: ServicesAccessor) { return accessor.get(IInstantiationService).createInstance(CommentsAccessibilityHelpProvider); } + dispose() { } } diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 5ba6ca59e04..09aeddd25e6 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -430,6 +430,7 @@ export class CommentController implements IEditorContribution { private _commentingRangeSpaceReserved = false; private _commentingRangeAmountReserved = 0; private _computePromise: CancelablePromise> | null; + private _computeAndSetPromise: Promise | undefined; private _addInProgress!: boolean; private _emptyThreadsToAddQueue: [Range | undefined, IEditorMouseEvent | undefined][] = []; private _computeCommentingRangePromise!: CancelablePromise | null; @@ -645,10 +646,12 @@ export class CommentController implements IEditorContribution { return Promise.resolve([]); }); - return this._computePromise.then(async commentInfos => { + this._computeAndSetPromise = this._computePromise.then(async commentInfos => { await this.setComments(coalesce(commentInfos)); this._computePromise = null; }, error => console.log(error)); + this._computePromise.then(() => this._computeAndSetPromise = undefined); + return this._computeAndSetPromise; } private beginComputeCommentingRanges() { @@ -687,8 +690,8 @@ export class CommentController implements IEditorContribution { if (commentThreadWidget.length === 1) { commentThreadWidget[0].reveal(commentUniqueId, focus); } else if (fetchOnceIfNotExist) { - if (this._computePromise) { - this._computePromise.then(_ => { + if (this._computeAndSetPromise) { + this._computeAndSetPromise.then(_ => { this.revealCommentThread(threadId, commentUniqueId, false, focus); }); } else { @@ -728,7 +731,7 @@ export class CommentController implements IEditorContribution { return; } - const after = this.editor.getSelection().getEndPosition(); + const after = reverse ? this.editor.getSelection().getStartPosition() : this.editor.getSelection().getEndPosition(); const sortedWidgets = this._commentWidgets.sort((a, b) => { if (reverse) { const temp = a; @@ -866,7 +869,8 @@ export class CommentController implements IEditorContribution { const pendingCommentText = (this._pendingNewCommentCache[uniqueOwner] && this._pendingNewCommentCache[uniqueOwner][thread.threadId]) ?? continueOnCommentText; const pendingEdits = this._pendingEditsCache[uniqueOwner] && this._pendingEditsCache[uniqueOwner][thread.threadId]; - const shouldReveal = thread.canReply && thread.isTemplate && (!thread.comments || (thread.comments.length === 0)) && (!thread.editorId || (thread.editorId === editorId)); + const isThreadTemplateOrEmpty = (thread.isTemplate || (!thread.comments || (thread.comments.length === 0))); + const shouldReveal = thread.canReply && isThreadTemplateOrEmpty && (!thread.editorId || (thread.editorId === editorId)); await this.displayCommentThread(uniqueOwner, thread, shouldReveal, pendingCommentText, pendingEdits); this._commentInfos.filter(info => info.uniqueOwner === uniqueOwner)[0].threads.push(thread); this.tryUpdateReservedSpace(); @@ -1128,7 +1132,7 @@ export class CommentController implements IEditorContribution { if (!newCommentInfos.length || !this.editor?.hasModel()) { this._addInProgress = false; if (!newCommentInfos.length) { - throw new Error('There are no commenting ranges at the current position.'); + throw new Error(`There are no commenting ranges at the current position (${range ? 'with range' : 'without range'}).`); } return Promise.resolve(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 6caf357feb6..7f0dfde0e64 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -35,7 +35,7 @@ import { CommentsModel } from 'vs/workbench/contrib/comments/browser/commentsMod import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IAction } from 'vs/base/common/actions'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -142,9 +142,9 @@ export class CommentsMenus implements IDisposable { @IMenuService private readonly menuService: IMenuService ) { } - getResourceActions(element: CommentNode): { menu?: IMenu; actions: IAction[] } { + getResourceActions(element: CommentNode): { actions: IAction[] } { const actions = this.getActions(MenuId.CommentsViewThreadActions, element); - return { menu: actions.menu, actions: actions.primary }; + return { actions: actions.primary }; } getResourceContextActions(element: CommentNode): IAction[] { @@ -155,7 +155,7 @@ export class CommentsMenus implements IDisposable { this.contextKeyService = service; } - private getActions(menuId: MenuId, element: CommentNode): { menu?: IMenu; primary: IAction[]; secondary: IAction[] } { + private getActions(menuId: MenuId, element: CommentNode): { primary: IAction[]; secondary: IAction[] } { if (!this.contextKeyService) { return { primary: [], secondary: [] }; } @@ -168,12 +168,11 @@ export class CommentsMenus implements IDisposable { ]; const contextKeyService = this.contextKeyService.createOverlay(overlay); - const menu = this.menuService.createMenu(menuId, contextKeyService); + const menu = this.menuService.getMenuActions(menuId, contextKeyService, { shouldForwardArgs: true }); const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary, menu }; - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, 'inline'); - menu.dispose(); + createAndFillInContextMenuActions(menu, result, 'inline'); return result; } @@ -303,7 +302,7 @@ export class CommentNodeRenderer implements IListRenderer const renderedComment = this.getRenderedComment(originalComment.comment.body, disposables); templateData.disposables.push(renderedComment); templateData.threadMetadata.commentPreview.appendChild(renderedComment.element.firstElementChild ?? renderedComment.element); - templateData.disposables.push(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), templateData.threadMetadata.commentPreview, renderedComment.element.textContent ?? '')); + templateData.disposables.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), templateData.threadMetadata.commentPreview, renderedComment.element.textContent ?? '')); } if (node.element.range) { diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index 938c658fd2d..8527341bdae 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -90,11 +90,14 @@ .comments-panel .comments-panel-container .tree-container .comment-thread-container .text * { margin: 0; text-overflow: ellipsis; - max-width: 500px; overflow: hidden; padding-right: 5px; } +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata .text * { + max-width: 700px; +} + .comments-panel .comments-panel-container .tree-container .comment-thread-container .range { opacity: 0.8; } diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 0ee64b833ea..f643c68547f 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -244,11 +244,6 @@ margin-top: 0; } -.review-widget .body .comment-body code { - border-radius: 3px; - padding: 0 0.4em; -} - .review-widget .body .comment-body span { white-space: pre; } diff --git a/src/vs/workbench/contrib/comments/browser/timestamp.ts b/src/vs/workbench/contrib/comments/browser/timestamp.ts index 47faa02f1c9..583ccaa7963 100644 --- a/src/vs/workbench/contrib/comments/browser/timestamp.ts +++ b/src/vs/workbench/contrib/comments/browser/timestamp.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { fromNow } from 'vs/base/common/date'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -18,7 +18,7 @@ export class TimestampWidget extends Disposable { private _timestamp: Date | undefined; private _useRelativeTime: boolean; - private hover: IUpdatableHover; + private hover: IManagedHover; constructor( private configurationService: IConfigurationService, @@ -30,7 +30,7 @@ export class TimestampWidget extends Disposable { this._date = dom.append(container, dom.$('span.timestamp')); this._date.style.display = 'none'; this._useRelativeTime = this.useRelativeTimeSetting; - this.hover = this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this._date, '')); + this.hover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this._date, '')); this.setTimestamp(timeStamp); } diff --git a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts index 84e77afe298..9c7d9e80c16 100644 --- a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts +++ b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IRange, Range } from 'vs/editor/common/core/range'; import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; diff --git a/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts b/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts index e903b24dc39..3f607d926f3 100644 --- a/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts +++ b/src/vs/workbench/contrib/contextmenu/browser/contextmenu.contribution.ts @@ -3,24 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -class ContextMenuContribution implements IWorkbenchContribution { - - private readonly disposables = new DisposableStore(); +class ContextMenuContribution extends Disposable implements IWorkbenchContribution { constructor( @ILayoutService layoutService: ILayoutService, @IContextMenuService contextMenuService: IContextMenuService ) { + super(); + const update = (visible: boolean) => layoutService.activeContainer.classList.toggle('context-menu-visible', visible); - contextMenuService.onDidShowContextMenu(() => update(true), null, this.disposables); - contextMenuService.onDidHideContextMenu(() => update(false), null, this.disposables); + this._register(contextMenuService.onDidShowContextMenu(() => update(true))); + this._register(contextMenuService.onDidHideContextMenu(() => update(false))); } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 6c99db87599..04b79c589ec 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -12,7 +12,6 @@ import { extname, isEqual } from 'vs/base/common/resources'; import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -24,7 +23,7 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { CONTEXT_ACTIVE_CUSTOM_EDITOR_ID, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorCapabilities, CustomEditorInfo, CustomEditorInfoCollection, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; -import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupContextKeyProvider, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorResolverService, IEditorType, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ContributedCustomEditors } from '../common/contributedCustomEditors'; @@ -40,16 +39,12 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ private readonly _models = new CustomEditorModelManager(); - private readonly _activeCustomEditorId: IContextKey; - private readonly _focusedCustomEditorIsEditable: IContextKey; - private readonly _onDidChangeEditorTypes = this._register(new Emitter()); public readonly onDidChangeEditorTypes: Event = this._onDidChangeEditorTypes.event; private readonly _fileEditorFactory = Registry.as(EditorExtensions.EditorFactory).getFileEditorFactory(); constructor( - @IContextKeyService contextKeyService: IContextKeyService, @IFileService fileService: IFileService, @IStorageService storageService: IStorageService, @IEditorService private readonly editorService: IEditorService, @@ -60,9 +55,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ) { super(); - this._activeCustomEditorId = CONTEXT_ACTIVE_CUSTOM_EDITOR_ID.bindTo(contextKeyService); - this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); - this._contributedEditors = this._register(new ContributedCustomEditors(storageService)); // Register the contribution points only emitting one change from the resolver this.editorResolverService.bufferChangeEvents(this.registerContributionPoints.bind(this)); @@ -70,10 +62,25 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._register(this._contributedEditors.onChange(() => { // Register the contribution points only emitting one change from the resolver this.editorResolverService.bufferChangeEvents(this.registerContributionPoints.bind(this)); - this.updateContexts(); this._onDidChangeEditorTypes.fire(); })); - this._register(this.editorService.onDidActiveEditorChange(() => this.updateContexts())); + + // Register group context key providers. + // These set the context keys for each editor group and the global context + const activeCustomEditorContextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: CONTEXT_ACTIVE_CUSTOM_EDITOR_ID, + getGroupContextKeyValue: group => this.getActiveCustomEditorId(group), + onDidChange: this.onDidChangeEditorTypes + }; + + const customEditorIsEditableContextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, + getGroupContextKeyValue: group => this.getCustomEditorIsEditable(group), + onDidChange: this.onDidChangeEditorTypes + }; + + this._register(this.editorGroupService.registerContextKeyProvider(activeCustomEditorContextKeyProvider)); + this._register(this.editorGroupService.registerContextKeyProvider(customEditorIsEditableContextKeyProvider)); this._register(fileService.onDidRunOperation(e => { if (e.isOperation(FileOperation.MOVE)) { @@ -88,8 +95,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._register(RedoCommand.addImplementation(PRIORITY, 'custom-editor', () => { return this.withActiveCustomEditor(editor => editor.redo()); })); - - this.updateContexts(); } getEditorTypes(): IEditorType[] { @@ -193,17 +198,24 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return this._editorCapabilities.get(viewType); } - private updateContexts() { - const activeEditorPane = this.editorService.activeEditorPane; + private getActiveCustomEditorId(group: IEditorGroup): string { + const activeEditorPane = group.activeEditorPane; const resource = activeEditorPane?.input?.resource; if (!resource) { - this._activeCustomEditorId.reset(); - this._focusedCustomEditorIsEditable.reset(); - return; + return ''; } - this._activeCustomEditorId.set(activeEditorPane?.input instanceof CustomEditorInput ? activeEditorPane.input.viewType : ''); - this._focusedCustomEditorIsEditable.set(activeEditorPane?.input instanceof CustomEditorInput); + return activeEditorPane?.input instanceof CustomEditorInput ? activeEditorPane.input.viewType : ''; + } + + private getCustomEditorIsEditable(group: IEditorGroup): boolean { + const activeEditorPane = group.activeEditorPane; + const resource = activeEditorPane?.input?.resource; + if (!resource) { + return false; + } + + return activeEditorPane?.input instanceof CustomEditorInput; } private async handleMovedFileInOpenedFileEditors(oldResource: URI, newResource: URI): Promise { diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 3546424023c..0515cc780f3 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -49,6 +49,7 @@ export interface IRenderValueOptions { export interface IVariableTemplateData { expression: HTMLElement; name: HTMLElement; + type: HTMLElement; value: HTMLElement; label: HighlightedLabel; lazyButton: HTMLElement; @@ -109,7 +110,7 @@ export function renderExpressionValue(expressionOrValue: IExpressionValue | stri if (options.hover) { const { store, commands, commandService } = options.hover instanceof DisposableStore ? { store: options.hover, commands: [], commandService: undefined } : options.hover; - store.add(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), container, () => { + store.add(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, () => { const container = dom.$('div'); const markdownHoverElement = dom.$('div.hover-row'); const hoverContentsElement = dom.append(markdownHoverElement, dom.$('div.hover-contents')); @@ -130,13 +131,20 @@ export function renderExpressionValue(expressionOrValue: IExpressionValue | stri } } -export function renderVariable(store: DisposableStore, commandService: ICommandService, hoverService: IHoverService, variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[], linkDetector?: LinkDetector): void { +export function renderVariable(store: DisposableStore, commandService: ICommandService, hoverService: IHoverService, variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[], linkDetector?: LinkDetector, displayType?: boolean): void { if (variable.available) { + data.type.textContent = ''; let text = variable.name; if (variable.value && typeof variable.name === 'string') { - text += ':'; + if (variable.type && displayType) { + text += ': '; + data.type.textContent = variable.type + ' ='; + } else { + text += ' ='; + } } - data.label.set(text, highlights, variable.type ? variable.type : variable.name); + + data.label.set(text, highlights, variable.type && !displayType ? variable.type : variable.name); data.name.classList.toggle('virtual', variable.presentationHint?.kind === 'virtual'); data.name.classList.toggle('internal', variable.presentationHint?.visibility === 'internal'); } else if (variable.value && typeof variable.name === 'string' && variable.name) { @@ -171,6 +179,7 @@ export interface IInputBoxOptions { export interface IExpressionTemplateData { expression: HTMLElement; name: HTMLSpanElement; + type: HTMLSpanElement; value: HTMLSpanElement; inputBoxContainer: HTMLElement; actionBar?: ActionBar; @@ -228,7 +237,10 @@ export abstract class AbstractExpressionsRenderer implements IT const name = dom.append(expression, $('span.name')); const lazyButton = dom.append(expression, $('span.lazy-button')); lazyButton.classList.add(...ThemeIcon.asClassNameArray(Codicon.eye)); - templateDisposable.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), lazyButton, localize('debug.lazyButton.tooltip', "Click to expand"))); + + templateDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), lazyButton, localize('debug.lazyButton.tooltip', "Click to expand"))); + const type = dom.append(expression, $('span.type')); + const value = dom.append(expression, $('span.value')); const label = templateDisposable.add(new HighlightedLabel(name)); @@ -241,7 +253,7 @@ export abstract class AbstractExpressionsRenderer implements IT actionBar = templateDisposable.add(new ActionBar(expression)); } - const template: IExpressionTemplateData = { expression, name, value, label, inputBoxContainer, actionBar, elementDisposable: new DisposableStore(), templateDisposable, lazyButton, currentElement: undefined }; + const template: IExpressionTemplateData = { expression, name, type, value, label, inputBoxContainer, actionBar, elementDisposable: new DisposableStore(), templateDisposable, lazyButton, currentElement: undefined }; templateDisposable.add(dom.addDisposableListener(lazyButton, dom.EventType.CLICK, () => { if (template.currentElement) { @@ -255,7 +267,6 @@ export abstract class AbstractExpressionsRenderer implements IT public abstract renderElement(node: ITreeNode, index: number, data: IExpressionTemplateData): void; protected renderExpressionElement(element: IExpression, node: ITreeNode, data: IExpressionTemplateData): void { - data.elementDisposable.clear(); data.currentElement = element; this.renderExpression(node.element, data, createMatches(node.filterData)); if (data.actionBar) { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 1b53d40047f..d29256d9072 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -869,8 +869,8 @@ registerThemingParticipant((theme, collector) => { } }); -export const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', { dark: '#E51400', light: '#E51400', hcDark: '#E51400', hcLight: '#E51400' }, nls.localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.')); -const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', { dark: '#848484', light: '#848484', hcDark: '#848484', hcLight: '#848484' }, nls.localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.')); -const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', { dark: '#848484', light: '#848484', hcDark: '#848484', hcLight: '#848484' }, nls.localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.')); +export const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', '#E51400', nls.localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.')); +const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', '#848484', nls.localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.')); +const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', '#848484', nls.localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.')); const debugIconBreakpointCurrentStackframeForeground = registerColor('debugIcon.breakpointCurrentStackframeForeground', { dark: '#FFCC00', light: '#BE8700', hcDark: '#FFCC00', hcLight: '#BE8700' }, nls.localize('debugIcon.breakpointCurrentStackframeForeground', 'Icon color for the current breakpoint stack frame.')); -const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', { dark: '#89D185', light: '#89D185', hcDark: '#89D185', hcLight: '#89D185' }, nls.localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.')); +const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', '#89D185', nls.localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.')); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 59a3a4dd4bf..11a82d72c3a 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -264,7 +264,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi } private createTriggerBreakpointInput(container: HTMLElement) { - const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint); + const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint && !bp.logMessage); const breakpointOptions: ISelectOptionItem[] = [ { text: nls.localize('noTriggerByBreakpoint', 'None'), isDisabled: true }, ...breakpoints.map(bp => ({ @@ -346,7 +346,10 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.toDispose.push(scopedContextKeyService); const scopedInstatiationService = this.instantiationService.createChild(new ServiceCollection( - [IContextKeyService, scopedContextKeyService], [IPrivateBreakpointWidgetService, this])); + [IContextKeyService, scopedContextKeyService], + [IPrivateBreakpointWidgetService, this] + )); + this.toDispose.push(scopedInstatiationService); const options = this.createEditorOptions(); const codeEditorWidgetOptions = getSimpleCodeEditorWidgetOptions(); @@ -433,12 +436,12 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi if (success) { // if there is already a breakpoint on this location - remove it. - let condition = this.breakpoint?.condition; - let hitCondition = this.breakpoint?.hitCondition; - let logMessage = this.breakpoint?.logMessage; - let triggeredBy = this.breakpoint?.triggeredBy; - let mode = this.breakpoint?.mode; - let modeLabel = this.breakpoint?.modeLabel; + let condition: string | undefined = undefined; + let hitCondition: string | undefined = undefined; + let logMessage: string | undefined = undefined; + let triggeredBy: string | undefined = undefined; + let mode: string | undefined = undefined; + let modeLabel: string | undefined = undefined; this.rememberInput(); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 8b989fb46f4..ac8dc3ed3d0 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -55,6 +55,7 @@ import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, In import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; const $ = dom.$; @@ -553,7 +554,7 @@ class BreakpointsRenderer implements IListRenderer { const debugService = accessor.get(IDebugService); + const viewService = accessor.get(IViewsService); + await viewService.openView(BREAKPOINTS_VIEW_ID); debugService.addFunctionBreakpoint(); } }); @@ -1643,7 +1646,7 @@ registerAction2(class extends Action2 { } else if (breakpoint instanceof DataBreakpoint) { await debugService.removeDataBreakpoints(breakpoint.getId()); } else if (breakpoint instanceof InstructionBreakpoint) { - await debugService.removeInstructionBreakpoints(breakpoint.instructionReference); + await debugService.removeInstructionBreakpoints(breakpoint.instructionReference, breakpoint.offset); } } }); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 80bdc5ffbd8..6ee016a167c 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -49,7 +49,7 @@ import { CALLSTACK_VIEW_ID, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_IT import { StackFrame, Thread, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; const $ = dom.$; @@ -136,7 +136,7 @@ async function expandTo(session: IDebugSession, tree: WorkbenchCompressibleAsync export class CallStackView extends ViewPane { private stateMessage!: HTMLSpanElement; private stateMessageLabel!: HTMLSpanElement; - private stateMessageLabelHover!: IUpdatableHover; + private stateMessageLabelHover!: IManagedHover; private onCallStackChangeScheduler: RunOnceScheduler; private needsRefresh = false; private ignoreSelectionChangedEvent = false; @@ -221,7 +221,7 @@ export class CallStackView extends ViewPane { this.stateMessage = dom.append(container, $('span.call-stack-state-message')); this.stateMessage.hidden = true; this.stateMessageLabel = dom.append(this.stateMessage, $('span.label')); - this.stateMessageLabelHover = this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.stateMessage, '')); + this.stateMessageLabelHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.stateMessage, '')); } protected override renderBody(container: HTMLElement): void { @@ -465,9 +465,8 @@ export class CallStackView extends ViewPane { const secondary: IAction[] = []; const result = { primary, secondary }; const contextKeyService = this.contextKeyService.createOverlay(overlay); - const menu = this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService); - createAndFillInContextMenuActions(menu, { arg: getContextForContributedActions(element), shouldForwardArgs: true }, result, 'inline'); - menu.dispose(); + const menu = this.menuService.getMenuActions(MenuId.DebugCallStackContext, contextKeyService, { arg: getContextForContributedActions(element), shouldForwardArgs: true }); + createAndFillInContextMenuActions(menu, result, 'inline'); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => result.secondary, @@ -582,7 +581,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer t.stopped); @@ -671,7 +670,7 @@ class ThreadsRenderer implements ICompressibleTreeRenderer, _index: number, data: IThreadTemplateData): void { const thread = element.element; - data.elementDisposable.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), data.thread, thread.name)); + data.elementDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.thread, thread.name)); data.label.set(thread.name, createMatches(element.filterData)); data.stateLabel.textContent = thread.stateLabel; data.stateLabel.classList.toggle('exception', thread.stoppedDetails?.reason === 'exception'); @@ -756,7 +755,7 @@ class StackFramesRenderer implements ICompressibleTreeRenderer, index: number, data: IErrorTemplateData): void { const error = element.element; data.label.textContent = error; - data.templateDisposable.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), data.label, error)); + data.templateDisposable.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.label, error)); } renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: IErrorTemplateData, height: number | undefined): void { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 36da9c868e0..b85da3db794 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -440,6 +440,11 @@ configurationRegistry.registerConfiguration({ title: nls.localize('debugConfigurationTitle', "Debug"), type: 'object', properties: { + 'debug.showVariableTypes': { + type: 'boolean', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'showVariableTypes' }, "Show variable type in variable pane during debug session"), + default: false + }, 'debug.allowBreakpointsEverywhere': { type: 'boolean', description: nls.localize({ comment: ['This is the description for a setting'], key: 'allowBreakpointsEverywhere' }, "Allow setting breakpoints in any file."), @@ -480,7 +485,7 @@ configurationRegistry.registerConfiguration({ }, 'debug.toolBarLocation': { enum: ['floating', 'docked', 'commandCenter', 'hidden'], - markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'toolBarLocation' }, "Controls the location of the debug toolbar. Either `floating` in all views, `docked` in the debug view, `commandCenter` (requires `{0}`), or `hidden`.", '#window.commandCenter#'), + markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'toolBarLocation' }, "Controls the location of the debug toolbar. Either `floating` in all views, `docked` in the debug view, `commandCenter` (requires {0}), or `hidden`.", '`#window.commandCenter#`'), default: 'floating', markdownEnumDescriptions: [ nls.localize('debugToolBar.floating', "Show debug toolbar in all views."), @@ -555,7 +560,8 @@ configurationRegistry.registerConfiguration({ type: 'object', description: nls.localize({ comment: ['This is the description for a setting'], key: 'launch' }, "Global debug launch configuration. Should be used as an alternative to 'launch.json' that is shared across workspaces."), default: { configurations: [], compounds: [] }, - $ref: launchSchemaId + $ref: launchSchemaId, + disallowConfigurationDefault: true }, 'debug.focusWindowOnBreak': { type: 'boolean', @@ -621,7 +627,7 @@ configurationRegistry.registerConfiguration({ }, 'debug.hideLauncherWhileDebugging': { type: 'boolean', - markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'debug.hideLauncherWhileDebugging' }, "Hide 'Start Debugging' control in title bar of 'Run and Debug' view while debugging is active. Only relevant when `{0}` is not `docked`.", '#debug.toolBarLocation#'), + markdownDescription: nls.localize({ comment: ['This is the description for a setting'], key: 'debug.hideLauncherWhileDebugging' }, "Hide 'Start Debugging' control in title bar of 'Run and Debug' view while debugging is active. Only relevant when {0} is not `docked`.", '`#debug.toolBarLocation#`'), default: false } } diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index a4d5c4e74e2..50a1b351fc2 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -78,7 +78,7 @@ export class StartDebugActionViewItem extends BaseActionViewItem { const keybinding = this.keybindingService.lookupKeybinding(this.action.id)?.getLabel(); const keybindingLabel = keybinding ? ` (${keybinding})` : ''; const title = this.action.label + keybindingLabel; - this.toDispose.push(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.start, title)); + this.toDispose.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.start, title)); this.start.setAttribute('role', 'button'); this.start.ariaLabel = title; diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts index f7b758666b1..19b47280524 100644 --- a/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -18,12 +18,7 @@ export const debugToolBarBackground = registerColor('debugToolBar.background', { hcLight: '#FFFFFF' }, localize('debugToolBarBackground', "Debug toolbar background color.")); -export const debugToolBarBorder = registerColor('debugToolBar.border', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('debugToolBarBorder', "Debug toolbar border color.")); +export const debugToolBarBorder = registerColor('debugToolBar.border', null, localize('debugToolBarBorder', "Debug toolbar border color.")); export const debugIconStartForeground = registerColor('debugIcon.startForeground', { dark: '#89D185', @@ -35,6 +30,7 @@ export const debugIconStartForeground = registerColor('debugIcon.startForeground export function registerColors() { const debugTokenExpressionName = registerColor('debugTokenExpression.name', { dark: '#c586c0', light: '#9b46b0', hcDark: foreground, hcLight: foreground }, 'Foreground color for the token names shown in the debug views (ie. the Variables or Watch view).'); + const debugTokenExpressionType = registerColor('debugTokenExpression.type', { dark: '#4A90E2', light: '#4A90E2', hcDark: foreground, hcLight: foreground }, 'Foreground color for the token types shown in the debug views (ie. the Variables or Watch view).'); const debugTokenExpressionValue = registerColor('debugTokenExpression.value', { dark: '#cccccc99', light: '#6c6c6ccc', hcDark: foreground, hcLight: foreground }, 'Foreground color for the token values shown in the debug views (ie. the Variables or Watch view).'); const debugTokenExpressionString = registerColor('debugTokenExpression.string', { dark: '#ce9178', light: '#a31515', hcDark: '#f48771', hcLight: '#a31515' }, 'Foreground color for strings in the debug views (ie. the Variables or Watch view).'); const debugTokenExpressionBoolean = registerColor('debugTokenExpression.boolean', { dark: '#4e94ce', light: '#0000ff', hcDark: '#75bdfe', hcLight: '#0000ff' }, 'Foreground color for booleans in the debug views (ie. the Variables or Watch view).'); @@ -43,15 +39,15 @@ export function registerColors() { const debugViewExceptionLabelForeground = registerColor('debugView.exceptionLabelForeground', { dark: foreground, light: '#FFF', hcDark: foreground, hcLight: foreground }, 'Foreground color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); const debugViewExceptionLabelBackground = registerColor('debugView.exceptionLabelBackground', { dark: '#6C2022', light: '#A31515', hcDark: '#6C2022', hcLight: '#A31515' }, 'Background color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); - const debugViewStateLabelForeground = registerColor('debugView.stateLabelForeground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, 'Foreground color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); - const debugViewStateLabelBackground = registerColor('debugView.stateLabelBackground', { dark: '#88888844', light: '#88888844', hcDark: '#88888844', hcLight: '#88888844' }, 'Background color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); - const debugViewValueChangedHighlight = registerColor('debugView.valueChangedHighlight', { dark: '#569CD6', light: '#569CD6', hcDark: '#569CD6', hcLight: '#569CD6' }, 'Color used to highlight value changes in the debug views (ie. in the Variables view).'); + const debugViewStateLabelForeground = registerColor('debugView.stateLabelForeground', foreground, 'Foreground color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); + const debugViewStateLabelBackground = registerColor('debugView.stateLabelBackground', '#88888844', 'Background color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); + const debugViewValueChangedHighlight = registerColor('debugView.valueChangedHighlight', '#569CD6', 'Color used to highlight value changes in the debug views (ie. in the Variables view).'); const debugConsoleInfoForeground = registerColor('debugConsole.infoForeground', { dark: editorInfoForeground, light: editorInfoForeground, hcDark: foreground, hcLight: foreground }, 'Foreground color for info messages in debug REPL console.'); const debugConsoleWarningForeground = registerColor('debugConsole.warningForeground', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: '#008000', hcLight: editorWarningForeground }, 'Foreground color for warning messages in debug REPL console.'); - const debugConsoleErrorForeground = registerColor('debugConsole.errorForeground', { dark: errorForeground, light: errorForeground, hcDark: errorForeground, hcLight: errorForeground }, 'Foreground color for error messages in debug REPL console.'); - const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, 'Foreground color for source filenames in debug REPL console.'); - const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, 'Foreground color for debug console input marker icon.'); + const debugConsoleErrorForeground = registerColor('debugConsole.errorForeground', errorForeground, 'Foreground color for error messages in debug REPL console.'); + const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', foreground, 'Foreground color for source filenames in debug REPL console.'); + const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', foreground, 'Foreground color for debug console input marker icon.'); const debugIconPauseForeground = registerColor('debugIcon.pauseForeground', { dark: '#75BEFF', @@ -210,6 +206,7 @@ export function registerColors() { } const tokenNameColor = theme.getColor(debugTokenExpressionName)!; + const tokenTypeColor = theme.getColor(debugTokenExpressionType)!; const tokenValueColor = theme.getColor(debugTokenExpressionValue)!; const tokenStringColor = theme.getColor(debugTokenExpressionString)!; const tokenBooleanColor = theme.getColor(debugTokenExpressionBoolean)!; @@ -221,6 +218,10 @@ export function registerColors() { color: ${tokenNameColor}; } + .monaco-workbench .monaco-list-row .expression .type { + color: ${tokenTypeColor}; + } + .monaco-workbench .monaco-list-row .expression .value, .monaco-workbench .debug-hover-widget .value { color: ${tokenValueColor}; diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index 7810995bb38..4f613027aaf 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -14,6 +14,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import * as nls from 'vs/nls'; +import { ILocalizedString } from 'vs/platform/action/common/action'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -22,14 +23,14 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { PanelFocusContext } from 'vs/workbench/common/contextkeys'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpointsView'; import { DisassemblyView } from 'vs/workbench/contrib/debug/browser/disassemblyView'; -import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE, CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_EXCEPTION_WIDGET_VISIBLE, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_IN_DEBUG_MODE, CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, IDebugConfiguration, IDebugEditorContribution, IDebugService, REPL_VIEW_ID, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { Repl } from 'vs/workbench/contrib/debug/browser/repl'; +import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_EXCEPTION_WIDGET_VISIBLE, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_IN_DEBUG_MODE, CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, IDebugConfiguration, IDebugEditorContribution, IDebugService, REPL_VIEW_ID, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { getEvaluatableExpressionAtPosition } from 'vs/workbench/contrib/debug/common/debugUtils'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ILocalizedString } from 'vs/platform/action/common/action'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; class ToggleBreakpointAction extends Action2 { constructor() { @@ -39,6 +40,7 @@ class ToggleBreakpointAction extends Action2 { ...nls.localize2('toggleBreakpointAction', "Debug: Toggle Breakpoint"), mnemonicTitle: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint"), }, + f1: true, precondition: CONTEXT_DEBUGGERS_AVAILABLE, keybinding: { when: ContextKeyExpr.or(EditorContextKeys.editorTextFocus, CONTEXT_DISASSEMBLY_VIEW_FOCUS), @@ -368,8 +370,8 @@ export class SelectionToReplAction extends EditorAction { text = editor.getModel().getValueInRange(selection); } - await session.addReplExpression(viewModel.focusedStackFrame, text); - await viewsService.openView(REPL_VIEW_ID, false); + const replView = await viewsService.openView(REPL_VIEW_ID, false) as Repl | undefined; + replView?.sendReplInput(text); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 4ab2ba9d777..5e3d5e9d583 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -6,7 +6,7 @@ import { addDisposableListener, isKeyboardEvent } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { distinct } from 'vs/base/common/arrays'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { memoize } from 'vs/base/common/decorators'; @@ -66,12 +66,7 @@ export const debugInlineForeground = registerColor('editor.inlineValuesForegroun hcLight: '#00000080' }, nls.localize('editor.inlineValuesForeground', "Color for the debug inline value text.")); -export const debugInlineBackground = registerColor('editor.inlineValuesBackground', { - dark: '#ffc80033', - light: '#ffc80033', - hcDark: '#ffc80033', - hcLight: '#ffc80033' -}, nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background.")); +export const debugInlineBackground = registerColor('editor.inlineValuesBackground', '#ffc80033', nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background.")); class InlineSegment { constructor(public column: number, public text: string) { @@ -126,7 +121,7 @@ function replaceWsWithNoBreakWs(str: string): string { return str.replace(/[ \t]/g, strings.noBreakWhitespace); } -function createInlineValueDecorationsInsideRange(expressions: ReadonlyArray, ranges: Range[], model: ITextModel, wordToLineNumbersMap: Map): IModelDeltaDecoration[] { +function createInlineValueDecorationsInsideRange(expressions: ReadonlyArray, ranges: Range[], model: ITextModel, wordToLineNumbersMap: Map) { const nameValueMap = new Map(); for (const expr of expressions) { nameValueMap.set(expr.name, expr.value); @@ -156,17 +151,14 @@ function createInlineValueDecorationsInsideRange(expressions: ReadonlyArray { - const contentText = names.sort((first, second) => { + return [...lineToNamesMap].map(([line, names]) => ({ + line, + variables: names.sort((first, second) => { const content = model.getLineContent(line); return content.indexOf(first) - content.indexOf(second); - }).map(name => `${name} = ${nameValueMap.get(name)}`).join(', '); - decorations.push(...createInlineValueDecoration(line, contentText)); - }); - - return decorations; + }).map(name => ({ name, value: nameValueMap.get(name)! })) + })); } function getWordToLineNumbersMap(model: ITextModel, lineNumber: number, result: Map) { @@ -208,7 +200,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { private toDispose: IDisposable[]; private hoverWidget: DebugHoverWidget; - private hoverPosition: Position | null = null; + private hoverPosition?: { position: Position; event: IMouseEvent }; private mouseDown = false; private exceptionWidgetVisible: IContextKey; private gutterIsHovered = false; @@ -341,7 +333,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { if (debugHoverWasVisible && this.hoverPosition) { // If the debug hover was visible immediately show the editor hover for the alt transition to be smooth - this.showEditorHover(this.hoverPosition, false); + this.showEditorHover(this.hoverPosition.position, false); } const onKeyUp = new DomEmitter(ownerDocument, 'keyup'); @@ -361,14 +353,14 @@ export class DebugEditorContribution implements IDebugEditorContribution { }); } - async showHover(position: Position, focus: boolean): Promise { + async showHover(position: Position, focus: boolean, mouseEvent?: IMouseEvent): Promise { // normally will already be set in `showHoverScheduler`, but public callers may hit this directly: this.preventDefaultEditorHover(); const sf = this.debugService.getViewModel().focusedStackFrame; const model = this.editor.getModel(); if (sf && model && this.uriIdentityService.extUri.isEqual(sf.source.uri, model.uri)) { - const result = await this.hoverWidget.showAt(position, focus); + const result = await this.hoverWidget.showAt(position, focus, mouseEvent); if (result === ShowDebugHoverResult.NOT_AVAILABLE) { // When no expression available fallback to editor hover this.showEditorHover(position, focus); @@ -438,7 +430,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { private get showHoverScheduler() { const scheduler = new RunOnceScheduler(() => { if (this.hoverPosition && !this.altPressed) { - this.showHover(this.hoverPosition, false); + this.showHover(this.hoverPosition.position, false, this.hoverPosition.event); } }, this.hoverDelay); this.toDispose.push(scheduler); @@ -493,8 +485,8 @@ export class DebugEditorContribution implements IDebugEditorContribution { } if (target.type === MouseTargetType.CONTENT_TEXT) { - if (target.position && !Position.equals(target.position, this.hoverPosition)) { - this.hoverPosition = target.position; + if (target.position && !Position.equals(target.position, this.hoverPosition?.position || null) && !this.hoverWidget.isInSafeTriangle(mouseEvent.event.posx, mouseEvent.event.posy)) { + this.hoverPosition = { position: target.position, event: mouseEvent.event }; // Disable the editor hover during the request to avoid flickering this.preventDefaultEditorHover(); this.showHoverScheduler.schedule(this.hoverDelay); @@ -782,10 +774,15 @@ export class DebugEditorContribution implements IDebugEditorContribution { // old "one-size-fits-all" strategy const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range); - // Get all top level variables in the scope chain - const decorationsPerScope = await Promise.all(scopes.map(async scope => { - const variables = await scope.getChildren(); + const scopesWithVariables = await Promise.all(scopes.map(async scope => + ({ scope, variables: await scope.getChildren() }))); + // Map of inline values per line that's populated in scope order, from + // narrowest to widest. This is done to avoid duplicating values if + // they appear in multiple scopes or are shadowed (#129770, #217326) + const valuesPerLine = new Map>(); + + for (const { scope, variables } of scopesWithVariables) { let scopeRange = new Range(0, 0, stackFrame.range.startLineNumber, stackFrame.range.startColumn); if (scope.range) { scopeRange = scopeRange.setStartPosition(scope.range.startLineNumber, scope.range.startColumn); @@ -797,12 +794,25 @@ export class DebugEditorContribution implements IDebugEditorContribution { this._wordToLineNumbersMap.ensureRangePopulated(range); } - return createInlineValueDecorationsInsideRange(variables, ownRanges, model, this._wordToLineNumbersMap.value); - })); + const mapped = createInlineValueDecorationsInsideRange(variables, ownRanges, model, this._wordToLineNumbersMap.value); + for (const { line, variables } of mapped) { + let values = valuesPerLine.get(line); + if (!values) { + values = new Map(); + valuesPerLine.set(line, values); + } - allDecorations = distinct(decorationsPerScope.flat(), - // Deduplicate decorations since same variable can appear in multiple scopes, leading to duplicated decorations #129770 - decoration => `${decoration.range.startLineNumber}:${decoration?.options.after?.content}`); + for (const { name, value } of variables) { + if (!values.has(name)) { + values.set(name, value); + } + } + } + } + + allDecorations = [...valuesPerLine.entries()].flatMap(([line, values]) => + createInlineValueDecoration(line, [...values].map(([n, v]) => `${n} = ${v}`).join(', ')) + ); } if (cts.token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index b534c3fba67..3b2bf385639 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -5,6 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -83,6 +84,7 @@ export class DebugHoverWidget implements IContentWidget { readonly allowEditorOverflow = true; private _isVisible: boolean; + private safeTriangle?: dom.SafeTriangle; private showCancellationSource?: CancellationTokenSource; private domNode!: HTMLElement; private tree!: AsyncDataTree; @@ -228,7 +230,15 @@ export class DebugHoverWidget implements IContentWidget { return this.domNode; } - async showAt(position: Position, focus: boolean): Promise { + /** + * Gets whether the given coordinates are in the safe triangle formed from + * the position at which the hover was initiated. + */ + isInSafeTriangle(x: number, y: number) { + return this._isVisible && !!this.safeTriangle?.contains(x, y); + } + + async showAt(position: Position, focus: boolean, mouseEvent?: IMouseEvent): Promise { this.showCancellationSource?.cancel(); const cancellationSource = this.showCancellationSource = new CancellationTokenSource(); const session = this.debugService.getViewModel().focusedSession; @@ -269,7 +279,7 @@ export class DebugHoverWidget implements IContentWidget { options: DebugHoverWidget._HOVER_HIGHLIGHT_DECORATION_OPTIONS }]); - return this.doShow(result.range.getStartPosition(), expression, focus); + return this.doShow(result.range.getStartPosition(), expression, focus, mouseEvent); } private static readonly _HOVER_HIGHLIGHT_DECORATION_OPTIONS = ModelDecorationOptions.register({ @@ -277,7 +287,7 @@ export class DebugHoverWidget implements IContentWidget { className: 'hoverHighlight' }); - private async doShow(position: Position, expression: IExpression, focus: boolean, forceValueHover = false): Promise { + private async doShow(position: Position, expression: IExpression, focus: boolean, mouseEvent: IMouseEvent | undefined): Promise { if (!this.domNode) { this.create(); } @@ -285,7 +295,7 @@ export class DebugHoverWidget implements IContentWidget { this.showAtPosition = position; this._isVisible = true; - if (!expression.hasChildren || forceValueHover) { + if (!expression.hasChildren) { this.complexValueContainer.hidden = true; this.valueContainer.hidden = false; renderExpressionValue(expression, this.valueContainer, { @@ -312,6 +322,7 @@ export class DebugHoverWidget implements IContentWidget { this.tree.scrollTop = 0; this.tree.scrollLeft = 0; this.complexValueContainer.hidden = false; + this.safeTriangle = mouseEvent && new dom.SafeTriangle(mouseEvent.posx, mouseEvent.posy, this.domNode); if (focus) { this.editor.render(); @@ -440,8 +451,10 @@ interface IDebugHoverComputeResult { } class DebugHoverComputer { - private _currentRange: Range | undefined; - private _currentExpression: string | undefined; + private _current?: { + range: Range; + expression: string; + }; constructor( private editor: ICodeEditor, @@ -463,30 +476,35 @@ class DebugHoverComputer { } const { range, matchingExpression } = result; - const rangeChanged = this._currentRange ? - !this._currentRange.equalsRange(range) : - true; - this._currentExpression = matchingExpression; - this._currentRange = Range.lift(range); - return { rangeChanged, range: this._currentRange }; + const rangeChanged = !this._current?.range.equalsRange(range); + this._current = { expression: matchingExpression, range: Range.lift(range) }; + return { rangeChanged, range: this._current.range }; } async evaluate(session: IDebugSession): Promise { - if (!this._currentExpression) { + if (!this._current) { this.logService.error('No expression to evaluate'); return; } + const textModel = this.editor.getModel(); + const debugSource = textModel && session.getSourceForUri(textModel?.uri); + if (session.capabilities.supportsEvaluateForHovers) { - const expression = new Expression(this._currentExpression); - await expression.evaluate(session, this.debugService.getViewModel().focusedStackFrame, 'hover'); + const expression = new Expression(this._current.expression); + await expression.evaluate(session, this.debugService.getViewModel().focusedStackFrame, 'hover', undefined, debugSource ? { + line: this._current.range.startLineNumber, + column: this._current.range.startColumn, + source: debugSource.raw, + } : undefined); return expression; } else { const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (focusedStackFrame) { return await findExpressionInStackFrame( focusedStackFrame, - coalesce(this._currentExpression.split('.').map(word => word.trim()))); + coalesce(this._current.expression.split('.').map(word => word.trim())) + ); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index d09eada1d2a..8c52537a0ae 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -51,6 +51,7 @@ import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -112,6 +113,7 @@ export class DebugService implements IDebugService { @IQuickInputService private readonly quickInputService: IQuickInputService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ITestService private readonly testService: ITestService, ) { this.breakpointsToSendOnResourceSaved = new Set(); @@ -202,8 +204,8 @@ export class DebugService implements IDebugService { this.disposables.add(extensionService.onWillStop(evt => { evt.veto( - this.stopSession(undefined).then(() => false), - nls.localize('stoppingDebug', 'Stopping debug sessions...'), + this.model.getSessions().length > 0, + nls.localize('active debug session', 'A debug session is still running.'), ); })); @@ -839,6 +841,21 @@ export class DebugService implements IDebugService { } }; + // For debug sessions spawned by test runs, cancel the test run and stop + // the session, then start the test run again; tests have no notion of restarts. + if (session.correlatedTestRun) { + if (!session.correlatedTestRun.completedAt) { + this.testService.cancelTestRun(session.correlatedTestRun.id); + await Event.toPromise(session.correlatedTestRun.onComplete); + // todo@connor4312 is there any reason to wait for the debug session to + // terminate? I don't think so, test extension should already handle any + // state conflicts... + } + + this.testService.runResolvedTests(session.correlatedTestRun.request); + return; + } + if (session.capabilities.supportsRestartRequest) { const taskResult = await runTasks(); if (taskResult === TaskRunResult.Success) { diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index e2d54e9a296..79c3cc8c123 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -42,6 +42,9 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { getActiveWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; import { isDefined } from 'vs/base/common/types'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; @@ -66,6 +69,11 @@ export class DebugSession implements IDebugSession, IDisposable { private stoppedDetails: IRawStoppedDetails[] = []; private readonly statusQueue = this.rawListeners.add(new ThreadStatusScheduler()); + /** Test run this debug session was spawned by */ + public readonly correlatedTestRun?: LiveTestResult; + /** Whether we terminated the correlated run yet. Used so a 2nd terminate request goes through to the underlying session. */ + private didTerminateTestRun?: boolean; + private readonly _onDidChangeState = new Emitter(); private readonly _onDidEndAdapter = new Emitter(); @@ -106,7 +114,9 @@ export class DebugSession implements IDebugSession, IDisposable { @IInstantiationService private readonly instantiationService: IInstantiationService, @ICustomEndpointTelemetryService private readonly customEndpointTelemetryService: ICustomEndpointTelemetryService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @ITestService private readonly testService: ITestService, + @ITestResultService testResultService: ITestResultService, ) { this._options = options || {}; this.parentSession = this._options.parentSession; @@ -126,6 +136,16 @@ export class DebugSession implements IDebugSession, IDisposable { })); } + // Cast here, it's not possible to reference a hydrated result in this code path. + this.correlatedTestRun = options?.testRun + ? (testResultService.getResult(options.testRun.runId) as LiveTestResult) + : this.parentSession?.correlatedTestRun; + + if (this.correlatedTestRun) { + // Listen to the test completing because the user might have taken the cancel action rather than stopping the session. + toDispose.add(this.correlatedTestRun.onComplete(() => this.terminate())); + } + const compoundRoot = this._options.compoundRoot; if (compoundRoot) { toDispose.add(compoundRoot.onDidSessionStop(() => this.terminate())); @@ -387,6 +407,9 @@ export class DebugSession implements IDebugSession, IDisposable { this.cancelAllRequests(); if (this._options.lifecycleManagedByParent && this.parentSession) { await this.parentSession.terminate(restart); + } else if (this.correlatedTestRun && !this.correlatedTestRun.completedAt && !this.didTerminateTestRun) { + this.didTerminateTestRun = true; + this.testService.cancelTestRun(this.correlatedTestRun.id); } else if (this.raw) { if (this.raw.capabilities.supportsTerminateRequest && this._configuration.resolved.request === 'launch') { await this.raw.terminate(restart); @@ -662,12 +685,12 @@ export class DebugSession implements IDebugSession, IDisposable { return this.raw.variables({ variablesReference, filter, start, count }, token); } - evaluate(expression: string, frameId: number, context?: string): Promise { + evaluate(expression: string, frameId: number, context?: string, location?: { line: number; column: number; source: DebugProtocol.Source }): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'evaluate')); } - return this.raw.evaluate({ expression, frameId, context }); + return this.raw.evaluate({ expression, frameId, context, line: location?.line, column: location?.column, source: location?.source }); } async restartFrame(frameId: number, threadId: number): Promise { @@ -1498,8 +1521,8 @@ export class DebugSession implements IDebugSession, IDisposable { this.repl.removeReplExpressions(); } - async addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise { - await this.repl.addReplExpression(this, stackFrame, name); + async addReplExpression(stackFrame: IStackFrame | undefined, expression: string): Promise { + await this.repl.addReplExpression(this, stackFrame, expression); // Evaluate all watch expressions and fetch variables again since repl evaluation might have changed some. this.debugService.getViewModel().updateViews(); } diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index add88e75098..7711f4ccc48 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -10,7 +10,7 @@ import { Action, IAction, IRunEvent, WorkbenchActionExecutedClassification, Work import * as arrays from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as errors from 'vs/base/common/errors'; -import { DisposableStore, dispose, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable, markAsSingleton, MutableDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/debugToolBar'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; @@ -351,9 +351,9 @@ export function createDisconnectMenuItemAction(action: MenuItemAction, disposabl const instantiationService = accessor.get(IInstantiationService); const contextMenuService = accessor.get(IContextMenuService); - const menu = menuService.createMenu(MenuId.DebugToolBarStop, contextKeyService); + const menu = menuService.getMenuActions(MenuId.DebugToolBarStop, contextKeyService, { shouldForwardArgs: true }); const secondary: IAction[] = []; - createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, secondary); + createAndFillInActionBarActions(menu, secondary); if (!secondary.length) { return undefined; @@ -401,7 +401,7 @@ const registerDebugToolBarItem = (id: string, title: string | ICommandActionTitl })); }; -MenuRegistry.onDidChangeMenu(e => { +markAsSingleton(MenuRegistry.onDidChangeMenu(e => { // In case the debug toolbar is docked we need to make sure that the docked toolbar has the up to date commands registered #115945 if (e.has(MenuId.DebugToolBar)) { dispose(debugViewTitleItems); @@ -413,7 +413,7 @@ MenuRegistry.onDidChangeMenu(e => { })); } } -}); +})); const CONTEXT_TOOLBAR_COMMAND_CENTER = ContextKeyExpr.equals('config.debug.toolBarLocation', 'commandCenter'); diff --git a/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts b/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts index 1b920801dec..7c9b48503a2 100644 --- a/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts @@ -24,7 +24,7 @@ const $ = dom.$; // theming -const debugExceptionWidgetBorder = registerColor('debugExceptionWidget.border', { dark: '#a31515', light: '#a31515', hcDark: '#a31515', hcLight: '#a31515' }, nls.localize('debugExceptionWidgetBorder', 'Exception widget border color.')); +const debugExceptionWidgetBorder = registerColor('debugExceptionWidget.border', '#a31515', nls.localize('debugExceptionWidgetBorder', 'Exception widget border color.')); const debugExceptionWidgetBackground = registerColor('debugExceptionWidget.background', { dark: '#420b0d', light: '#f1dfde', hcDark: '#420b0d', hcLight: '#f1dfde' }, nls.localize('debugExceptionWidgetBackground', 'Exception widget background color.')); export class ExceptionWidget extends ZoneWidget { diff --git a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css index fbe36b38c1a..fc948f97b4a 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css @@ -5,7 +5,7 @@ .monaco-workbench .debug-toolbar { position: absolute; - z-index: 3000; + z-index: 2520; /* Below quick input at 2550, above custom titlebar toolbar at 2500 */ height: 26px; display: flex; padding-left: 7px; diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 5baf2c48fb2..59696ad5004 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -52,7 +52,7 @@ display: flex; } -.monaco-workbench .repl .repl-tree .output.expression.value-and-source .value { +.monaco-workbench .repl .repl-tree .output.expression.value-and-source .label { margin-right: 4px; } @@ -71,7 +71,8 @@ left: 2px; } -.monaco-workbench .repl .repl-tree .output.expression.value-and-source .source { +.monaco-workbench .repl .repl-tree .output.expression.value-and-source .source, +.monaco-workbench .repl .repl-tree .group .source { margin-left: auto; margin-right: 8px; cursor: pointer; diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 4f183f3701a..c282aa3fab9 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -151,7 +151,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.menu = menuService.createMenu(MenuId.DebugConsoleContext, contextKeyService); this._register(this.menu); - this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); + this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 100); this.filter = new ReplFilter(); this.filter.filterQuery = filterText; this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService); @@ -472,6 +472,15 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { } } + sendReplInput(input: string): void { + const session = this.tree?.getInput(); + if (session && !this.isReadonly) { + session.addReplExpression(this.debugService.getViewModel().focusedStackFrame, input); + revealLastElement(this.tree!); + this.history.add(input); + } + } + getVisibleContent(): string { let text = ''; if (this.model && this.tree) { @@ -687,7 +696,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { }; CONTEXT_IN_DEBUG_REPL.bindTo(this.scopedContextKeyService).set(true); - this.scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + this.scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); const options = getSimpleEditorOptions(this.configurationService); options.readOnly = true; options.suggest = { showStatusBar: true }; diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index bae5c31696c..50d81a8f041 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -6,20 +6,25 @@ import * as dom from 'vs/base/browser/dom'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { IManagedHover } from 'vs/base/browser/ui/hover/hover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/path'; import severity from 'vs/base/common/severity'; +import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ThemeIcon } from 'vs/base/common/themables'; import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderExpressionValue, renderVariable } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; import { debugConsoleEvaluationInput } from 'vs/workbench/contrib/debug/browser/debugIcons'; @@ -28,9 +33,6 @@ import { IDebugConfiguration, IDebugService, IDebugSession, IExpression, IExpres import { Variable } from 'vs/workbench/contrib/debug/common/debugModel'; import { RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult, ReplGroup, ReplOutputElement, ReplVariableElement } from 'vs/workbench/contrib/debug/common/replModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; const $ = dom.$; @@ -40,6 +42,7 @@ interface IReplEvaluationInputTemplateData { interface IReplGroupTemplateData { label: HTMLElement; + source: SourceWidget; } interface IReplEvaluationResultTemplateData { @@ -51,9 +54,8 @@ interface IOutputReplElementTemplateData { count: CountBadge; countContainer: HTMLElement; value: HTMLElement; - source: HTMLElement; + source: SourceWidget; getReplElementSource(): IReplElementSource | undefined; - toDispose: IDisposable[]; elementListener: IDisposable; } @@ -94,7 +96,8 @@ export class ReplGroupRenderer implements ITreeRenderer, _index: number, templateData: IReplGroupTemplateData): void { @@ -111,10 +117,11 @@ export class ReplGroupRenderer implements ITreeRenderer { - e.preventDefault(); - e.stopPropagation(); - const source = data.getReplElementSource(); - if (source) { - source.source.openInEditor(this.editorService, { - startLineNumber: source.lineNumber, - startColumn: source.column, - endLineNumber: source.lineNumber, - endColumn: source.column - }); - } - })); + data.value = dom.append(expression, $('span.value.label')); + data.source = this.instaService.createInstance(SourceWidget, expression); return data; } @@ -204,8 +195,7 @@ export class ReplOutputElementRenderer implements ITreeRenderer element.sourceData; } @@ -219,7 +209,7 @@ export class ReplOutputElementRenderer implements ITreeRenderer, _index: number, templateData: IOutputReplElementTemplateData): void { @@ -247,6 +237,7 @@ export class ReplVariablesRenderer extends AbstractExpressionsRenderer, _index: number, data: IExpressionTemplateData): void { const element = node.element; + data.elementDisposable.clear(); super.renderExpressionElement(element instanceof ReplVariableElement ? element.expression : element, node, data); } @@ -431,3 +422,39 @@ export class ReplAccessibilityProvider implements IListAccessibilityProvider { + e.preventDefault(); + e.stopPropagation(); + if (this.source) { + this.source.source.openInEditor(editorService, { + startLineNumber: this.source.lineNumber, + startColumn: this.source.column, + endLineNumber: this.source.lineNumber, + endColumn: this.source.column + }); + } + })); + + } + + public setSource(source?: IReplElementSource) { + this.source = source; + this.el.textContent = source ? `${basename(source.source.name)}:${source.lineNumber}` : ''; + + this.hover ??= this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.el, '')); + this.hover.update(source ? `${this.labelService.getUriLabel(source.source.uri)}:${source.lineNumber}` : ''); + } +} diff --git a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts index 4231608a29b..54c881dfb53 100644 --- a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts +++ b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { ColorTransformType, asCssVariable, asCssVariableName, registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { asCssVariable, asCssVariableName, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDebugService, State, IDebugSession, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -31,21 +31,11 @@ export const STATUS_BAR_DEBUGGING_FOREGROUND = registerColor('statusBar.debuggin hcLight: '#FFFFFF' }, localize('statusBarDebuggingForeground', "Status bar foreground color when a program is being debugged. The status bar is shown in the bottom of the window")); -export const STATUS_BAR_DEBUGGING_BORDER = registerColor('statusBar.debuggingBorder', { - dark: STATUS_BAR_BORDER, - light: STATUS_BAR_BORDER, - hcDark: STATUS_BAR_BORDER, - hcLight: STATUS_BAR_BORDER -}, localize('statusBarDebuggingBorder', "Status bar border color separating to the sidebar and editor when a program is being debugged. The status bar is shown in the bottom of the window")); +export const STATUS_BAR_DEBUGGING_BORDER = registerColor('statusBar.debuggingBorder', STATUS_BAR_BORDER, localize('statusBarDebuggingBorder', "Status bar border color separating to the sidebar and editor when a program is being debugged. The status bar is shown in the bottom of the window")); export const COMMAND_CENTER_DEBUGGING_BACKGROUND = registerColor( 'commandCenter.debuggingBackground', - { - dark: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 }, - hcDark: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 }, - light: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 }, - hcLight: { value: STATUS_BAR_DEBUGGING_BACKGROUND, op: ColorTransformType.Transparent, factor: 0.258 } - }, + transparent(STATUS_BAR_DEBUGGING_BACKGROUND, 0.258), localize('commandCenter-activeBackground', "Command center background color when a program is being debugged"), true ); @@ -86,7 +76,7 @@ export class StatusBarColorProvider implements IWorkbenchContribution { if (e.affectsConfiguration('debug.enableStatusBarColor') || e.affectsConfiguration('debug.toolBarLocation')) { this.update(); } - }, this.disposables); + }, undefined, this.disposables); this.update(); } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 5dc2a66b073..7adb71aa317 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -16,7 +16,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -41,7 +41,7 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderExpressionValue, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { ADD_TO_WATCH_ID, ADD_TO_WATCH_LABEL, COPY_EVALUATE_PATH_ID, COPY_EVALUATE_PATH_LABEL, COPY_VALUE_ID, COPY_VALUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; -import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DataBreakpointSetType, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DataBreakpointSetType, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugConfiguration, IDebugService, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { getContextForVariable } from 'vs/workbench/contrib/debug/common/debugContext'; import { ErrorScope, Expression, Scope, StackFrame, Variable, VisualizedExpression, getUriForDebugMemory } from 'vs/workbench/contrib/debug/common/debugModel'; import { DebugVisualizer, IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -246,22 +246,16 @@ export async function openContextMenuForVariableTreeElement(parentContextKeyServ return; } - const toDispose = new DisposableStore(); + const contextKeyService = await getContextForVariableMenuWithDataAccess(parentContextKeyService, variable); + const context: IVariablesContext = getVariablesContext(variable); + const menu = menuService.getMenuActions(menuId, contextKeyService, { arg: context, shouldForwardArgs: false }); - try { - const contextKeyService = await getContextForVariableMenuWithDataAccess(parentContextKeyService, variable); - const menu = toDispose.add(menuService.createMenu(menuId, contextKeyService)); - - const context: IVariablesContext = getVariablesContext(variable); - const secondary: IAction[] = []; - createAndFillInContextMenuActions(menu, { arg: context, shouldForwardArgs: false }, { primary: [], secondary }, 'inline'); - contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => secondary - }); - } finally { - toDispose.dispose(); - } + const secondary: IAction[] = []; + createAndFillInContextMenuActions(menu, { primary: [], secondary }, 'inline'); + contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => secondary + }); } const getVariablesContext = (variable: Variable): IVariablesContext => ({ @@ -455,6 +449,7 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer { } public override renderElement(node: ITreeNode, index: number, data: IExpressionTemplateData): void { + data.elementDisposable.clear(); super.renderExpressionElement(node.element, node, data); } @@ -499,11 +494,11 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer { protected override renderActionBar(actionBar: ActionBar, expression: IExpression, _data: IExpressionTemplateData) { const viz = expression as VisualizedExpression; const contextKeyService = viz.original ? getContextForVariableMenuBase(this.contextKeyService, viz.original) : this.contextKeyService; - const menu = this.menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService); + const context = viz.original ? getVariablesContext(viz.original) : undefined; + const menu = this.menuService.getMenuActions(MenuId.DebugVariablesContext, contextKeyService, { arg: context, shouldForwardArgs: false }); const primary: IAction[] = []; - const context = viz.original ? getVariablesContext(viz.original) : undefined; - createAndFillInContextMenuActions(menu, { arg: context, shouldForwardArgs: false }, { primary, secondary: [] }, 'inline'); + createAndFillInContextMenuActions(menu, { primary, secondary: [] }, 'inline'); if (viz.original) { const action = new Action('debugViz', localize('removeVisualizer', 'Remove Visualizer'), ThemeIcon.asClassName(Codicon.eye), true, () => this.debugService.getViewModel().setVisualizedExpression(viz.original!, undefined)); @@ -531,6 +526,7 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, @IHoverService hoverService: IHoverService, + @IConfigurationService private configurationService: IConfigurationService, ) { super(debugService, contextViewService, hoverService); } @@ -540,10 +536,17 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { } protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void { - renderVariable(data.elementDisposable, this.commandService, this.hoverService, expression as Variable, data, true, highlights, this.linkDetector); + const showType = this.configurationService.getValue('debug').showVariableTypes; + renderVariable(data.elementDisposable, this.commandService, this.hoverService, expression as Variable, data, true, highlights, this.linkDetector, showType); } public override renderElement(node: ITreeNode, index: number, data: IExpressionTemplateData): void { + data.elementDisposable.clear(); + data.elementDisposable.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('debug.showVariableTypes')) { + super.renderExpressionElement(node.element, node, data); + } + })); super.renderExpressionElement(node.element, node, data); } @@ -574,11 +577,11 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { protected override renderActionBar(actionBar: ActionBar, expression: IExpression, data: IExpressionTemplateData) { const variable = expression as Variable; const contextKeyService = getContextForVariableMenuBase(this.contextKeyService, variable); - const menu = this.menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService); const primary: IAction[] = []; const context = getVariablesContext(variable); - createAndFillInContextMenuActions(menu, { arg: context, shouldForwardArgs: false }, { primary, secondary: [] }, 'inline'); + const menu = this.menuService.getMenuActions(MenuId.DebugVariablesContext, contextKeyService, { arg: context, shouldForwardArgs: false }); + createAndFillInContextMenuActions(menu, { primary, secondary: [] }, 'inline'); actionBar.clear(); actionBar.context = context; diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 7369a367607..22014a4ad96 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -34,7 +34,7 @@ import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionT import { watchExpressionsAdd, watchExpressionsRemoveAll } from 'vs/workbench/contrib/debug/browser/debugIcons'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { VariablesRenderer, VisualizedVariableRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; -import { CONTEXT_CAN_VIEW_MEMORY, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_WATCH_ITEM_TYPE, IDebugService, IExpression, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_CAN_VIEW_MEMORY, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_WATCH_ITEM_TYPE, IDebugConfiguration, IDebugService, IExpression, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, Variable, VisualizedExpression } from 'vs/workbench/contrib/debug/common/debugModel'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; @@ -157,7 +157,7 @@ export class WatchExpressionsView extends ViewPane { let horizontalScrolling: boolean | undefined; this._register(this.debugService.getViewModel().onDidSelectExpression(e => { const expression = e?.expression; - if (expression && this.tree.hasElement(expression)) { + if (expression && this.tree.hasNode(expression)) { horizontalScrolling = this.tree.options.horizontalScrolling; if (horizontalScrolling) { this.tree.updateOptions({ horizontalScrolling: false }); @@ -274,7 +274,7 @@ class WatchExpressionsDataSource extends AbstractExpressionDataSource, index: number, data: IExpressionTemplateData): void { + data.elementDisposable.clear(); + data.elementDisposable.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('debug.showVariableTypes')) { + super.renderExpressionElement(node.element, node, data); + } + })); super.renderExpressionElement(node.element, node, data); } protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void { - const text = typeof expression.value === 'string' ? `${expression.name}:` : expression.name; + let text: string; + data.type.textContent = ''; + const showType = this.configurationService.getValue('debug').showVariableTypes; + if (showType && expression.type) { + text = typeof expression.value === 'string' ? `${expression.name}: ` : expression.name; + //render type + data.type.textContent = expression.type + ' ='; + } else { + text = typeof expression.value === 'string' ? `${expression.name} =` : expression.name; + } + let title: string; if (expression.type) { - title = expression.type === expression.value ? - expression.type : - `${expression.type}: ${expression.value}`; + if (showType) { + title = `${expression.name}`; + } else { + title = expression.type === expression.value ? + expression.type : + `${expression.type}`; + } } else { title = expression.value; } @@ -352,11 +373,11 @@ class WatchExpressionsRenderer extends AbstractExpressionsRenderer { protected override renderActionBar(actionBar: ActionBar, expression: IExpression) { const contextKeyService = getContextForWatchExpressionMenu(this.contextKeyService, expression); - const menu = this.menuService.createMenu(MenuId.DebugWatchContext, contextKeyService); + const context = expression; + const menu = this.menuService.getMenuActions(MenuId.DebugWatchContext, contextKeyService, { arg: context, shouldForwardArgs: false }); const primary: IAction[] = []; - const context = expression; - createAndFillInContextMenuActions(menu, { arg: context, shouldForwardArgs: false }, { primary, secondary: [] }, 'inline'); + createAndFillInContextMenuActions(menu, { primary, secondary: [] }, 'inline'); actionBar.clear(); actionBar.context = context; diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index e48607b9eda..41f1a55b557 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -27,6 +27,7 @@ import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompou import { IDataBreakpointOptions, IFunctionBreakpointOptions, IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { ITaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; +import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export const VIEWLET_ID = 'workbench.view.debug'; @@ -50,9 +51,9 @@ export const CONTEXT_IN_DEBUG_REPL = new RawContextKey('inDebugRepl', f export const CONTEXT_BREAKPOINT_WIDGET_VISIBLE = new RawContextKey('breakpointWidgetVisible', false, { type: 'boolean', description: nls.localize('breakpointWidgetVisibile', "True when breakpoint editor zone widget is visible, false otherwise.") }); export const CONTEXT_IN_BREAKPOINT_WIDGET = new RawContextKey('inBreakpointWidget', false, { type: 'boolean', description: nls.localize('inBreakpointWidget', "True when focus is in the breakpoint editor zone widget, false otherwise.") }); export const CONTEXT_BREAKPOINTS_FOCUSED = new RawContextKey('breakpointsFocused', true, { type: 'boolean', description: nls.localize('breakpointsFocused', "True when the BREAKPOINTS view is focused, false otherwise.") }); -export const CONTEXT_WATCH_EXPRESSIONS_FOCUSED = new RawContextKey('watchExpressionsFocused', true, { type: 'boolean', description: nls.localize('watchExpressionsFocused', "True when the WATCH view is focused, false otherwsie.") }); +export const CONTEXT_WATCH_EXPRESSIONS_FOCUSED = new RawContextKey('watchExpressionsFocused', true, { type: 'boolean', description: nls.localize('watchExpressionsFocused', "True when the WATCH view is focused, false otherwise.") }); export const CONTEXT_WATCH_EXPRESSIONS_EXIST = new RawContextKey('watchExpressionsExist', false, { type: 'boolean', description: nls.localize('watchExpressionsExist', "True when at least one watch expression exists, false otherwise.") }); -export const CONTEXT_VARIABLES_FOCUSED = new RawContextKey('variablesFocused', true, { type: 'boolean', description: nls.localize('variablesFocused', "True when the VARIABLES views is focused, false otherwsie") }); +export const CONTEXT_VARIABLES_FOCUSED = new RawContextKey('variablesFocused', true, { type: 'boolean', description: nls.localize('variablesFocused', "True when the VARIABLES views is focused, false otherwise") }); export const CONTEXT_EXPRESSION_SELECTED = new RawContextKey('expressionSelected', false, { type: 'boolean', description: nls.localize('expressionSelected', "True when an expression input box is open in either the WATCH or the VARIABLES view, false otherwise.") }); export const CONTEXT_BREAKPOINT_INPUT_FOCUSED = new RawContextKey('breakpointInputFocused', false, { type: 'boolean', description: nls.localize('breakpointInputFocused', "True when the input box has focus in the BREAKPOINTS view.") }); export const CONTEXT_CALLSTACK_ITEM_TYPE = new RawContextKey('callStackItemType', undefined, { type: 'string', description: nls.localize('callStackItemType', "Represents the item type of the focused element in the CALL STACK view. For example: 'session', 'thread', 'stackFrame'") }); @@ -71,7 +72,7 @@ export const CONTEXT_FOCUSED_SESSION_IS_ATTACH = new RawContextKey('foc export const CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG = new RawContextKey('focusedSessionIsNoDebug', false, { type: 'boolean', description: nls.localize('focusedSessionIsNoDebug', "True when the focused session is run without debugging.") }); export const CONTEXT_STEP_BACK_SUPPORTED = new RawContextKey('stepBackSupported', false, { type: 'boolean', description: nls.localize('stepBackSupported', "True when the focused session supports 'stepBack' requests.") }); export const CONTEXT_RESTART_FRAME_SUPPORTED = new RawContextKey('restartFrameSupported', false, { type: 'boolean', description: nls.localize('restartFrameSupported', "True when the focused session supports 'restartFrame' requests.") }); -export const CONTEXT_STACK_FRAME_SUPPORTS_RESTART = new RawContextKey('stackFrameSupportsRestart', false, { type: 'boolean', description: nls.localize('stackFrameSupportsRestart', "True when the focused stack frame suppots 'restartFrame'.") }); +export const CONTEXT_STACK_FRAME_SUPPORTS_RESTART = new RawContextKey('stackFrameSupportsRestart', false, { type: 'boolean', description: nls.localize('stackFrameSupportsRestart', "True when the focused stack frame supports 'restartFrame'.") }); export const CONTEXT_JUMP_TO_CURSOR_SUPPORTED = new RawContextKey('jumpToCursorSupported', false, { type: 'boolean', description: nls.localize('jumpToCursorSupported', "True when the focused session supports 'jumpToCursor' request.") }); export const CONTEXT_STEP_INTO_TARGETS_SUPPORTED = new RawContextKey('stepIntoTargetsSupported', false, { type: 'boolean', description: nls.localize('stepIntoTargetsSupported', "True when the focused session supports 'stepIntoTargets' request.") }); export const CONTEXT_BREAKPOINTS_EXIST = new RawContextKey('breakpointsExist', false, { type: 'boolean', description: nls.localize('breakpointsExist', "True when at least one breakpoint exists.") }); @@ -219,6 +220,11 @@ export interface LoadedSourceEvent { export type IDebugSessionReplMode = 'separate' | 'mergeWithParent'; +export interface IDebugTestRunReference { + runId: string; + taskId: string; +} + export interface IDebugSessionOptions { noDebug?: boolean; parentSession?: IDebugSession; @@ -231,6 +237,11 @@ export interface IDebugSessionOptions { suppressDebugToolbar?: boolean; suppressDebugStatusbar?: boolean; suppressDebugView?: boolean; + /** + * Set if the debug session is correlated with a test run. Stopping/restarting + * the session will instead stop/restart the test run. + */ + testRun?: IDebugTestRunReference; } export interface IDataBreakpointInfoResponse { @@ -335,6 +346,12 @@ export interface INewReplElementData { source?: IReplElementSource; } +export interface IDebugEvaluatePosition { + line: number; + column: number; + source: DebugProtocol.Source; +} + export interface IDebugSession extends ITreeElement { @@ -353,6 +370,8 @@ export interface IDebugSession extends ITreeElement { readonly suppressDebugStatusbar: boolean; readonly suppressDebugView: boolean; readonly lifecycleManagedByParent: boolean; + /** Test run this debug session was spawned by */ + readonly correlatedTestRun?: LiveTestResult; setSubId(subId: string | undefined): void; @@ -418,7 +437,7 @@ export interface IDebugSession extends ITreeElement { exceptionInfo(threadId: number): Promise; scopes(frameId: number, threadId: number): Promise; variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise; - evaluate(expression: string, frameId?: number, context?: string): Promise; + evaluate(expression: string, frameId?: number, context?: string, location?: IDebugEvaluatePosition): Promise; customRequest(request: string, args: any): Promise; cancel(progressId: string): Promise; disassemble(memoryReference: string, offset: number, instructionOffset: number, instructionCount: number): Promise; @@ -534,7 +553,8 @@ export interface IStackFrame extends ITreeElement { } export function isFrameDeemphasized(frame: IStackFrame): boolean { - return frame.source.presentationHint === 'deemphasize' || frame.presentationHint === 'deemphasize' || frame.presentationHint === 'subtle'; + const hint = frame.presentationHint ?? frame.source.presentationHint; + return hint === 'deemphasize' || hint === 'subtle'; } export interface IEnablement extends ITreeElement { @@ -774,6 +794,7 @@ export interface IDebugConfiguration { }; autoExpandLazyVariables: boolean; enableStatusBarColor: boolean; + showVariableTypes: boolean; } export interface IGlobalConfig { diff --git a/src/vs/workbench/contrib/debug/common/debugLifecycle.ts b/src/vs/workbench/contrib/debug/common/debugLifecycle.ts index 838140a91d0..68420965735 100644 --- a/src/vs/workbench/contrib/debug/common/debugLifecycle.ts +++ b/src/vs/workbench/contrib/debug/common/debugLifecycle.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IDisposable } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -11,13 +12,15 @@ import { IDebugConfiguration, IDebugService } from 'vs/workbench/contrib/debug/c import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; export class DebugLifecycle implements IWorkbenchContribution { + private disposable: IDisposable; + constructor( @ILifecycleService lifecycleService: ILifecycleService, @IDebugService private readonly debugService: IDebugService, @IConfigurationService private readonly configurationService: IConfigurationService, @IDialogService private readonly dialogService: IDialogService, ) { - lifecycleService.onBeforeShutdown(async e => e.veto(this.shouldVetoShutdown(e.reason), 'veto.debug')); + this.disposable = lifecycleService.onBeforeShutdown(async e => e.veto(this.shouldVetoShutdown(e.reason), 'veto.debug')); } private shouldVetoShutdown(_reason: ShutdownReason): boolean | Promise { @@ -34,6 +37,10 @@ export class DebugLifecycle implements IWorkbenchContribution { return this.showWindowCloseConfirmation(rootSessions.length); } + public dispose() { + return this.disposable.dispose(); + } + private async showWindowCloseConfirmation(numSessions: number): Promise { let message: string; if (numSessions === 1) { diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 13f7d1d902d..f9c67ec8958 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -22,7 +22,7 @@ import * as nls from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { DEBUG_MEMORY_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State, isFrameDeemphasized } from 'vs/workbench/contrib/debug/common/debug'; +import { DEBUG_MEMORY_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugEvaluatePosition, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State, isFrameDeemphasized } from 'vs/workbench/contrib/debug/common/debug'; import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -198,7 +198,9 @@ export class ExpressionContainer implements IExpressionContainer { session: IDebugSession | undefined, stackFrame: IStackFrame | undefined, context: string, - keepLazyVars = false): Promise { + keepLazyVars = false, + location?: IDebugEvaluatePosition, + ): Promise { if (!session || (!stackFrame && context !== 'repl')) { this.value = context === 'repl' ? nls.localize('startDebugFirst', "Please start a debug session to evaluate expressions") : Expression.DEFAULT_VALUE; @@ -208,7 +210,7 @@ export class ExpressionContainer implements IExpressionContainer { this.session = session; try { - const response = await session.evaluate(expression, stackFrame ? stackFrame.frameId : undefined, context); + const response = await session.evaluate(expression, stackFrame ? stackFrame.frameId : undefined, context, location); if (response && response.body) { this.value = response.body.result || ''; @@ -306,8 +308,8 @@ export class Expression extends ExpressionContainer implements IExpression { } } - async evaluate(session: IDebugSession | undefined, stackFrame: IStackFrame | undefined, context: string, keepLazyVars?: boolean): Promise { - this.available = await this.evaluateExpression(this.name, session, stackFrame, context, keepLazyVars); + async evaluate(session: IDebugSession | undefined, stackFrame: IStackFrame | undefined, context: string, keepLazyVars?: boolean, location?: IDebugEvaluatePosition): Promise { + this.available = await this.evaluateExpression(this.name, session, stackFrame, context, keepLazyVars, location); } override toString(): string { @@ -540,6 +542,8 @@ export class StackFrame implements IStackFrame { } } +const KEEP_SUBTLE_FRAME_AT_TOP_REASONS: readonly string[] = ['breakpoint', 'step', 'function breakpoint']; + export class Thread implements IThread { private callStack: IStackFrame[]; private staleCallStack: IStackFrame[]; @@ -578,10 +582,11 @@ export class Thread implements IThread { getTopStackFrame(): IStackFrame | undefined { const callStack = this.getCallStack(); + const stopReason = this.stoppedDetails?.reason; // Allow stack frame without source and with instructionReferencePointer as top stack frame when using disassembly view. const firstAvailableStackFrame = callStack.find(sf => !!( - ((this.stoppedDetails?.reason === 'instruction breakpoint' || (this.stoppedDetails?.reason === 'step' && this.lastSteppingGranularity === 'instruction')) && sf.instructionPointerReference) || - (sf.source && sf.source.available && !isFrameDeemphasized(sf)))); + ((stopReason === 'instruction breakpoint' || (stopReason === 'step' && this.lastSteppingGranularity === 'instruction')) && sf.instructionPointerReference) || + (sf.source && sf.source.available && (KEEP_SUBTLE_FRAME_AT_TOP_REASONS.includes(stopReason!) || !isFrameDeemphasized(sf))))); return firstAvailableStackFrame; } @@ -1544,7 +1549,13 @@ export class DebugModel extends Disposable implements IDebugModel { let topCallStack = Promise.resolve(); const wholeCallStack = new Promise((c, e) => { topCallStack = thread.fetchCallStack(1).then(() => { - if (!this.schedulers.has(thread.getId()) && fetchFullStack) { + if (!fetchFullStack) { + c(); + this._onDidChangeCallStack.fire(); + return; + } + + if (!this.schedulers.has(thread.getId())) { const deferred = new DeferredPromise(); this.schedulers.set(thread.getId(), { completeDeferred: deferred, diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 50eacfd65e2..963e3d553e4 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -1377,6 +1377,15 @@ declare module DebugProtocol { expression: string; /** Evaluate the expression in the scope of this stack frame. If not specified, the expression is evaluated in the global scope. */ frameId?: number; + /** The contextual line where the expression should be evaluated. In the 'hover' context, this should be set to the start of the expression being hovered. */ + line?: number; + /** The contextual column where the expression should be evaluated. This may be provided if `line` is also provided. + + It is measured in UTF-16 code units and the client capability `columnsStartAt1` determines whether it is 0- or 1-based. + */ + column?: number; + /** The contextual source in which the `line` is found. This must be provided if `line` is provided. */ + source?: Source; /** The context in which the evaluate request is used. Values: 'watch': evaluate is called from a watch view context. @@ -2401,7 +2410,7 @@ declare module DebugProtocol { Values: 'source': In `SourceBreakpoint`s 'exception': In exception breakpoints applied in the `ExceptionFilterOptions` - 'data': In data breakpoints requested in the the `DataBreakpointInfo` request + 'data': In data breakpoints requested in the `DataBreakpointInfo` request 'instruction': In `InstructionBreakpoint`s etc. */ diff --git a/src/vs/workbench/contrib/debug/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index acf1e467817..1b193bd9d5d 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -102,7 +102,7 @@ export function getExactExpressionStartAndEnd(lineContent: string, looseStart: n // If there are non-word characters after the cursor, we want to truncate the expression then. // For example in expression 'a.b.c.d', if the focus was under 'b', 'a.b' would be evaluated. if (matchingExpression) { - const subExpression: RegExp = /\w+/g; + const subExpression: RegExp = /(\w|\p{L})+/gu; let subExpressionResult: RegExpExecArray | null = null; while (subExpressionResult = subExpression.exec(matchingExpression)) { const subEnd = subExpressionResult.index + 1 + startOffset + subExpressionResult[0].length; diff --git a/src/vs/workbench/contrib/debug/common/debugVisualizers.ts b/src/vs/workbench/contrib/debug/common/debugVisualizers.ts index 45d19aee6f9..47baa50ee41 100644 --- a/src/vs/workbench/contrib/debug/common/debugVisualizers.ts +++ b/src/vs/workbench/contrib/debug/common/debugVisualizers.ts @@ -7,7 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; import { isDefined } from 'vs/base/common/types'; import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ExtensionIdentifier, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { CONTEXT_VARIABLE_NAME, CONTEXT_VARIABLE_TYPE, CONTEXT_VARIABLE_VALUE, MainThreadDebugVisualization, IDebugVisualization, IDebugVisualizationContext, IExpression, IExpressionContainer, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; @@ -250,7 +250,7 @@ export class DebugVisualizerService implements IDebugVisualizerService { return context; } - private processExtensionRegistration(ext: Readonly) { + private processExtensionRegistration(ext: IExtensionDescription) { const viz = ext.contributes?.debugVisualizers; if (!(viz instanceof Array)) { return; diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index 4402ed3f3b5..6556849e5e9 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -269,10 +269,10 @@ export class ReplModel { return this.replElements; } - async addReplExpression(session: IDebugSession, stackFrame: IStackFrame | undefined, name: string): Promise { - this.addReplElement(new ReplEvaluationInput(name)); - const result = new ReplEvaluationResult(name); - await result.evaluateExpression(name, session, stackFrame, 'repl'); + async addReplExpression(session: IDebugSession, stackFrame: IStackFrame | undefined, expression: string): Promise { + this.addReplElement(new ReplEvaluationInput(expression)); + const result = new ReplEvaluationResult(expression); + await result.evaluateExpression(expression, session, stackFrame, 'repl'); this.addReplElement(result); } diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index 075791be12a..2f591514d5a 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as dom from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isWindows } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullCommandService } from 'vs/platform/commands/test/common/nullCommandService'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { NullHoverService } from 'vs/platform/hover/test/browser/nullHoverService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { renderExpressionValue, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; @@ -23,6 +24,66 @@ import { MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; const $ = dom.$; +function assertVariable(session: MockSession, scope: Scope, disposables: Pick, linkDetector: LinkDetector, displayType: boolean) { + let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'); + let expression = $('.'); + let name = $('.'); + let type = $('.'); + let value = $('.'); + const label = new HighlightedLabel(name); + const lazyButton = $('.'); + const store = disposables.add(new DisposableStore()); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], undefined, displayType); + + assert.strictEqual(label.element.textContent, 'foo'); + assert.strictEqual(value.textContent, ''); + + variable.value = 'hey'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); + assert.strictEqual(value.textContent, 'hey'); + assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); + assert.strictEqual(type.textContent, displayType ? 'string =' : ''); + + variable.value = isWindows ? 'C:\\foo.js:5' : '/foo.js:5'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); + assert.ok(value.querySelector('a')); + assert.strictEqual(value.querySelector('a')!.textContent, variable.value); + + variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, undefined, { kind: 'virtual' }); + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); + assert.strictEqual(name.className, 'virtual'); + assert.strictEqual(label.element.textContent, 'console ='); + assert.strictEqual(value.className, 'value number'); + + variable = new Variable(session, 1, scope, 2, 'xpto', 'xpto.xpto', undefined, 0, 0, undefined, {}, 'custom-type'); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); + assert.strictEqual(label.element.textContent, 'xpto'); + assert.strictEqual(value.textContent, ''); + variable.value = '2'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); + assert.strictEqual(value.textContent, '2'); + assert.strictEqual(label.element.textContent, displayType ? 'xpto: ' : 'xpto ='); + assert.strictEqual(type.textContent, displayType ? 'custom-type =' : ''); + + label.dispose(); +} + suite('Debug - Base Debug View', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let linkDetector: LinkDetector; @@ -33,6 +94,7 @@ suite('Debug - Base Debug View', () => { setup(() => { const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); linkDetector = instantiationService.createInstance(LinkDetector); + instantiationService.stub(IHoverService, NullHoverService); }); test('render view tree', () => { @@ -42,7 +104,7 @@ suite('Debug - Base Debug View', () => { assert.strictEqual(treeContainer.className, 'debug-view-content'); assert.strictEqual(container.childElementCount, 1); assert.strictEqual(container.firstChild, treeContainer); - assert.strictEqual(treeContainer instanceof HTMLDivElement, true); + assert.strictEqual(dom.isHTMLDivElement(treeContainer), true); }); test('render expression value', () => { @@ -85,47 +147,32 @@ suite('Debug - Base Debug View', () => { test('render variable', () => { const session = new MockSession(); const thread = new Thread(session, 'mockthread', 1); - const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: undefined!, endColumn: undefined! }, 0, true); + const range = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: undefined!, + endColumn: undefined! + }; + const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); - let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'); - let expression = $('.'); - let name = $('.'); - let value = $('.'); - const label = new HighlightedLabel(name); - const lazyButton = $('.'); - const store = disposables.add(new DisposableStore()); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, value, label, lazyButton }, false, []); + assertVariable(session, scope, disposables, linkDetector, false); - assert.strictEqual(label.element.textContent, 'foo'); - assert.strictEqual(value.textContent, ''); + }); - variable.value = 'hey'; - expression = $('.'); - name = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, value, label, lazyButton }, false, [], linkDetector); - assert.strictEqual(value.textContent, 'hey'); - assert.strictEqual(label.element.textContent, 'foo:'); + test('render variable with display type setting', () => { + const session = new MockSession(); + const thread = new Thread(session, 'mockthread', 1); + const range = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: undefined!, + endColumn: undefined! + }; + const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); + const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); - variable.value = isWindows ? 'C:\\foo.js:5' : '/foo.js:5'; - expression = $('.'); - name = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, value, label, lazyButton }, false, [], linkDetector); - assert.ok(value.querySelector('a')); - assert.strictEqual(value.querySelector('a')!.textContent, variable.value); - - variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, undefined, { kind: 'virtual' }); - expression = $('.'); - name = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, value, label, lazyButton }, false, [], linkDetector); - assert.strictEqual(name.className, 'virtual'); - assert.strictEqual(label.element.textContent, 'console:'); - assert.strictEqual(value.className, 'value number'); - - label.dispose(); + assertVariable(session, scope, disposables, linkDetector, true); }); test('statusbar in debug mode', () => { diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index 6b47aa1dc3f..292fa730381 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { dispose } from 'vs/base/common/lifecycle'; import { URI as uri } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index f5ec3d56c5a..e4473594e4c 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { ThemeIcon } from 'vs/base/common/themables'; import { Constants } from 'vs/base/common/uint'; @@ -41,7 +41,7 @@ export function createTestSession(model: DebugModel, name = 'mockSession', optio } }; } - } as IDebugService, undefined!, undefined!, new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }), undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService()); + } as IDebugService, undefined!, undefined!, new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }), undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService(), undefined!, undefined!); } function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame; secondStackFrame: StackFrame } { @@ -445,7 +445,7 @@ suite('Debug - CallStack', () => { override get state(): State { return State.Stopped; } - }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService()); + }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService(), undefined!, undefined!); disposables.add(session); const runningSession = createTestSession(model); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts index c6901f77f85..41c662c091b 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; +import { isHTMLSpanElement } from 'vs/base/browser/dom'; import { Color, RGBA } from 'vs/base/common/color'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { generateUuid } from 'vs/base/common/uuid'; @@ -66,7 +67,7 @@ suite('Debug - ANSI Handling', () => { assert.strictEqual(2, root.children.length); child = root.firstChild!; - if (child instanceof HTMLSpanElement) { + if (isHTMLSpanElement(child)) { assert.strictEqual('content1', child.textContent); assert(child.classList.contains('class1')); assert(child.classList.contains('class2')); @@ -75,7 +76,7 @@ suite('Debug - ANSI Handling', () => { } child = root.lastChild!; - if (child instanceof HTMLSpanElement) { + if (isHTMLSpanElement(child)) { assert.strictEqual('content2', child.textContent); assert(child.classList.contains('class2')); assert(child.classList.contains('class3')); @@ -94,7 +95,7 @@ suite('Debug - ANSI Handling', () => { const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, themeService, session.root); assert.strictEqual(1, root.children.length); const child: Node = root.lastChild!; - if (child instanceof HTMLSpanElement) { + if (isHTMLSpanElement(child)) { return child; } else { assert.fail('Unexpected assertion error'); @@ -408,7 +409,7 @@ suite('Debug - ANSI Handling', () => { assert.strictEqual(elementsExpected, root.children.length); for (let i = 0; i < elementsExpected; i++) { const child: Node = root.children[i]; - if (child instanceof HTMLSpanElement) { + if (isHTMLSpanElement(child)) { assertions[i](child); } else { assert.fail('Unexpected assertion error'); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts index 71b53f1ab25..6cde637373c 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts index c45e6dba417..480c811fc87 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { NullLogService } from 'vs/platform/log/common/log'; import { findExpressionInStackFrame } from 'vs/workbench/contrib/debug/browser/debugHover'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts index 7647820f3d0..61e92664e50 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; import { Emitter } from 'vs/base/common/event'; import { mockObject, MockObject } from 'vs/base/test/common/mock'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugSession.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugSession.test.ts index a4b59eee38d..b58f2509a7b 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugSession.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugSession.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ThreadStatusScheduler } from 'vs/workbench/contrib/debug/browser/debugSession'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts index 672ae3c3061..aad31249a2c 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugSource.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { URI as uri } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts index 585497db087..d6b033ff16f 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugUtils.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfig } from 'vs/workbench/contrib/debug/common/debug'; import { formatPII, getExactExpressionStartAndEnd, getVisibleAndSorted } from 'vs/workbench/contrib/debug/common/debugUtils'; @@ -41,6 +41,9 @@ suite('Debug - Utils', () => { assert.deepStrictEqual(getExactExpressionStartAndEnd('var t = a.b;c.d.name', 16, 20), { start: 13, end: 20 }); assert.deepStrictEqual(getExactExpressionStartAndEnd('var t = a.b.c-d.name', 16, 20), { start: 15, end: 20 }); + + assert.deepStrictEqual(getExactExpressionStartAndEnd('var aøñéå文 = a.b.c-d.name', 5, 5), { start: 5, end: 10 }); + assert.deepStrictEqual(getExactExpressionStartAndEnd('aøñéå文.aøñéå文.aøñéå文', 9, 9), { start: 1, end: 13 }); }); test('config presentation', () => { diff --git a/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts index d42de786cb5..6e2f76448ec 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugViewModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts index e389f76ae26..1b888a6e147 100644 --- a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; +import { isHTMLAnchorElement } from 'vs/base/browser/dom'; import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -33,7 +34,7 @@ suite('Debug - Link Detector', () => { * @param element The Element to verify. */ function assertElementIsLink(element: Element) { - assert(element instanceof HTMLAnchorElement); + assert(isHTMLAnchorElement(element)); } test('noLinks', () => { diff --git a/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts b/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts index 3f3549c604e..5a9f3f2f415 100644 --- a/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock, mockObject } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts index f385cbbb7b3..54da8d61f19 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { timeout } from 'vs/base/common/async'; import severity from 'vs/base/common/severity'; diff --git a/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts b/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts new file mode 100644 index 00000000000..5eb511bc678 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as dom from 'vs/base/browser/dom'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { Scope, StackFrame, Thread, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; +import { MockDebugService, MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { NullHoverService } from 'vs/platform/hover/test/browser/nullHoverService'; +import { IDebugService, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; +import { VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; +import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +const $ = dom.$; + +function assertVariable(disposables: Pick, variablesRenderer: VariablesRenderer, displayType: boolean) { + const session = new MockSession(); + const thread = new Thread(session, 'mockthread', 1); + const range = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: undefined!, + endColumn: undefined! + }; + const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); + const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); + const node = { + element: new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'), + depth: 0, + visibleChildrenCount: 1, + visibleChildIndex: -1, + collapsible: false, + collapsed: false, + visible: true, + filterData: undefined, + children: [] + }; + const expression = $('.'); + const name = $('.'); + const type = $('.'); + const value = $('.'); + const label = disposables.add(new HighlightedLabel(name)); + const lazyButton = $('.'); + const inputBoxContainer = $('.'); + const elementDisposable = disposables.add(new DisposableStore()); + const templateDisposable = disposables.add(new DisposableStore()); + const currentElement = undefined; + const data = { + expression, + name, + type, + value, + label, + lazyButton, + inputBoxContainer, + elementDisposable, + templateDisposable, + currentElement + }; + variablesRenderer.renderElement(node, 0, data); + assert.strictEqual(value.textContent, ''); + assert.strictEqual(label.element.textContent, 'foo'); + + node.element.value = 'xpto'; + variablesRenderer.renderElement(node, 0, data); + assert.strictEqual(value.textContent, 'xpto'); + assert.strictEqual(type.textContent, displayType ? 'string =' : ''); + assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); +} + +suite('Debug - Variable Debug View', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + let variablesRenderer: VariablesRenderer; + let instantiationService: TestInstantiationService; + let linkDetector: LinkDetector; + let configurationService: TestConfigurationService; + + setup(() => { + instantiationService = workbenchInstantiationService(undefined, disposables); + linkDetector = instantiationService.createInstance(LinkDetector); + const debugService = new MockDebugService(); + instantiationService.stub(IHoverService, NullHoverService); + debugService.getViewModel = () => { focusedStackFrame: undefined, getSelectedExpression: () => undefined }; + debugService.getViewModel().getSelectedExpression = () => undefined; + instantiationService.stub(IDebugService, debugService); + }); + + test('variable expressions with display type', () => { + configurationService = new TestConfigurationService({ + debug: { + showVariableTypes: true + } + }); + instantiationService.stub(IConfigurationService, configurationService); + variablesRenderer = instantiationService.createInstance(VariablesRenderer, linkDetector); + assertVariable(disposables, variablesRenderer, true); + }); + + test('variable expressions', () => { + configurationService = new TestConfigurationService({ + debug: { + showVariableTypes: false + } + }); + instantiationService.stub(IConfigurationService, configurationService); + variablesRenderer = instantiationService.createInstance(VariablesRenderer, linkDetector); + assertVariable(disposables, variablesRenderer, false); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/browser/watch.test.ts b/src/vs/workbench/contrib/debug/test/browser/watch.test.ts index dabf666859c..92b54afe7ca 100644 --- a/src/vs/workbench/contrib/debug/test/browser/watch.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/watch.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DebugModel, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; import { createMockDebugModel } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; diff --git a/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts b/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts new file mode 100644 index 00000000000..220e79d6a84 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as dom from 'vs/base/browser/dom'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { WatchExpressionsRenderer } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; +import { Scope, StackFrame, Thread, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; +import { MockDebugService, MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { NullHoverService } from 'vs/platform/hover/test/browser/nullHoverService'; +import { IDebugService, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +const $ = dom.$; + +function assertWatchVariable(disposables: Pick, watchExpressionsRenderer: WatchExpressionsRenderer, displayType: boolean) { + const session = new MockSession(); + const thread = new Thread(session, 'mockthread', 1); + const range = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: undefined!, + endColumn: undefined! + }; + const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); + const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); + const node = { + element: new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'), + depth: 0, + visibleChildrenCount: 1, + visibleChildIndex: -1, + collapsible: false, + collapsed: false, + visible: true, + filterData: undefined, + children: [] + }; + const expression = $('.'); + const name = $('.'); + const type = $('.'); + const value = $('.'); + const label = disposables.add(new HighlightedLabel(name)); + const lazyButton = $('.'); + const inputBoxContainer = $('.'); + const elementDisposable = disposables.add(new DisposableStore()); + const templateDisposable = disposables.add(new DisposableStore()); + const currentElement = undefined; + const data = { + expression, + name, + type, + value, + label, + lazyButton, + inputBoxContainer, + elementDisposable, + templateDisposable, + currentElement + }; + watchExpressionsRenderer.renderElement(node, 0, data); + assert.strictEqual(value.textContent, ''); + assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); + + node.element.value = 'xpto'; + watchExpressionsRenderer.renderElement(node, 0, data); + assert.strictEqual(value.textContent, 'xpto'); + assert.strictEqual(type.textContent, displayType ? 'string =' : ''); + assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); +} + +suite('Debug - Watch Debug View', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + let watchExpressionsRenderer: WatchExpressionsRenderer; + let instantiationService: TestInstantiationService; + let configurationService: TestConfigurationService; + + setup(() => { + instantiationService = workbenchInstantiationService(undefined, disposables); + const debugService = new MockDebugService(); + instantiationService.stub(IHoverService, NullHoverService); + debugService.getViewModel = () => { focusedStackFrame: undefined, getSelectedExpression: () => undefined }; + debugService.getViewModel().getSelectedExpression = () => undefined; + instantiationService.stub(IDebugService, debugService); + }); + + test('watch expressions with display type', () => { + configurationService = new TestConfigurationService({ + debug: { + showVariableTypes: true + } + }); + instantiationService.stub(IConfigurationService, configurationService); + watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer); + assertWatchVariable(disposables, watchExpressionsRenderer, true); + }); + + test('watch expressions', () => { + configurationService = new TestConfigurationService({ + debug: { + showVariableTypes: false + } + }); + instantiationService.stub(IConfigurationService, configurationService); + watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer); + assertWatchVariable(disposables, watchExpressionsRenderer, false); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts b/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts index 2246b7fa248..cfcc1af8b87 100644 --- a/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts +++ b/src/vs/workbench/contrib/debug/test/common/abstractDebugAdapter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MockDebugAdapter } from 'vs/workbench/contrib/debug/test/common/mockDebug'; diff --git a/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts index 26c5549841b..e3cf88f9b07 100644 --- a/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DeferredPromise } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mockObject } from 'vs/base/test/common/mock'; diff --git a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts index 4d2d38f1f65..e618a4552da 100644 --- a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { join, normalize } from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { IDebugAdapterExecutable, IConfig, IDebugSession, IAdapterManager } from 'vs/workbench/contrib/debug/common/debug'; @@ -64,7 +64,8 @@ suite('Debug - Debugger', () => { 'debuggers': [ debuggerContribution ] - } + }, + enabledApiProposals: undefined, }; const extensionDescriptor1 = { @@ -89,7 +90,8 @@ suite('Debug - Debugger', () => { args: ['parg'] } ] - } + }, + enabledApiProposals: undefined, }; const extensionDescriptor2 = { @@ -122,7 +124,8 @@ suite('Debug - Debugger', () => { } } ] - } + }, + enabledApiProposals: undefined, }; diff --git a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts index 9247a0ee636..89bb6968f5e 100644 --- a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as crypto from 'crypto'; import * as net from 'net'; import * as platform from 'vs/base/common/platform'; diff --git a/src/vs/workbench/contrib/debug/test/node/terminals.test.ts b/src/vs/workbench/contrib/debug/test/node/terminals.test.ts index efdf2f940c3..bd81c9c13e2 100644 --- a/src/vs/workbench/contrib/debug/test/node/terminals.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/terminals.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { prepareCommand } from 'vs/workbench/contrib/debug/node/terminals'; diff --git a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts index c6be96b073d..244f3dfe5bc 100644 --- a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts +++ b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts @@ -20,7 +20,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { mock } from 'vs/base/test/common/mock'; import * as sinon from 'sinon'; -import * as assert from 'assert'; +import assert from 'assert'; import { ChangeType, FileType, IEditSessionsLogService, IEditSessionsStorageService } from 'vs/workbench/contrib/editSessions/common/editSessions'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts index f3a0b28bf04..76d14f29f54 100644 --- a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts +++ b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts @@ -5,7 +5,7 @@ import { IGrammarContributions, EmmetEditorAction } from 'vs/workbench/contrib/emmet/browser/emmetActions'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/encryption/electron-sandbox/encryption.contribution.ts b/src/vs/workbench/contrib/encryption/electron-sandbox/encryption.contribution.ts index 9969928a2b5..48580f61c82 100644 --- a/src/vs/workbench/contrib/encryption/electron-sandbox/encryption.contribution.ts +++ b/src/vs/workbench/contrib/encryption/electron-sandbox/encryption.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isLinux } from 'vs/base/common/platform'; -import { stripComments } from 'vs/base/common/stripComments'; +import { parse } from 'vs/base/common/jsonc'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -34,7 +34,7 @@ class EncryptionContribution implements IWorkbenchContribution { } try { const content = await this.fileService.readFile(this.environmentService.argvResource); - const argv = JSON.parse(stripComments(content.value.toString())); + const argv = parse(content.value.toString()); if (argv['password-store'] === 'gnome' || argv['password-store'] === 'gnome-keyring') { this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['password-store'], value: 'gnome-libsecret' }], true); } diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 1ee623c22a0..a298cf3c232 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -369,14 +369,14 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { } else { title = nls.localize('extensionActivating', "Extension is activating..."); } - data.elementDisposables.push(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), data.activationTime, title)); + data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.activationTime, title)); clearNode(data.msgContainer); if (this._getUnresponsiveProfile(element.description.identifier)) { const el = $('span', undefined, ...renderLabelWithIcons(` $(alert) Unresponsive`)); const extensionHostFreezTitle = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze."); - data.elementDisposables.push(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), el, extensionHostFreezTitle)); + data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), el, extensionHostFreezTitle)); data.msgContainer.appendChild(el); } @@ -425,7 +425,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { const element = $('span', undefined, `${nls.localize('requests count', "{0} Requests: {1} (Overall)", feature.label, accessData.totalCount)}${accessData.current ? nls.localize('session requests count', ", {0} (Session)", accessData.current.count) : ''}`); if (accessData.current) { const title = nls.localize('requests count title', "Last request was {0}.", fromNow(accessData.current.lastAccessed, true, true)); - data.elementDisposables.push(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), element, title)); + data.elementDisposables.push(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, title)); } data.msgContainer.appendChild(element); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index dc3c89dcdc8..d3109d7f7d9 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -49,11 +49,11 @@ import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { ExtensionFeaturesTab } from 'vs/workbench/contrib/extensions/browser/extensionFeaturesTab'; import { - ActionWithDropDownAction, + ButtonWithDropDownExtensionAction, ClearLanguageAction, DisableDropDownAction, EnableDropDownAction, - ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, + ButtonWithDropdownExtensionActionViewItem, DropDownExtensionAction, ExtensionEditorManageExtensionAction, ExtensionStatusAction, ExtensionStatusLabelAction, @@ -71,7 +71,7 @@ import { UninstallAction, UpdateAction, WebInstallAction, - TogglePreReleaseExtensionAction + TogglePreReleaseExtensionAction, } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { Delegate } from 'vs/workbench/contrib/extensions/browser/extensionsList'; import { ExtensionData, ExtensionsGridView, ExtensionsTree, getExtensions } from 'vs/workbench/contrib/extensions/browser/extensionsViewer'; @@ -200,7 +200,7 @@ class VersionWidget extends ExtensionWithDifferentGalleryVersionWidget { ) { super(); this.element = append(container, $('code.version')); - this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.element, localize('extension version', "Extension Version"))); + this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('extension version', "Extension Version"))); this.render(); } render(): void { @@ -287,11 +287,11 @@ export class ExtensionEditor extends EditorPane { const details = append(header, $('.details')); const title = append(details, $('.title')); const name = append(title, $('span.name.clickable', { role: 'heading', tabIndex: 0 })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), name, localize('name', "Extension name"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), name, localize('name', "Extension name"))); const versionWidget = new VersionWidget(title, this.hoverService); const preview = append(title, $('span.preview')); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), preview, localize('preview', "Preview"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), preview, localize('preview', "Preview"))); preview.textContent = localize('preview', "Preview"); const builtin = append(title, $('span.builtin')); @@ -299,7 +299,7 @@ export class ExtensionEditor extends EditorPane { const subtitle = append(details, $('.subtitle')); const publisher = append(append(subtitle, $('.subtitle-entry')), $('.publisher.clickable', { tabIndex: 0 })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), publisher, localize('publisher', "Publisher"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), publisher, localize('publisher', "Publisher"))); publisher.setAttribute('role', 'button'); const publisherDisplayName = append(publisher, $('.publisher-name')); const verifiedPublisherWidget = this.instantiationService.createInstance(VerifiedPublisherWidget, append(publisher, $('.verified-publisher')), false); @@ -308,11 +308,11 @@ export class ExtensionEditor extends EditorPane { resource.setAttribute('role', 'button'); const installCount = append(append(subtitle, $('.subtitle-entry')), $('span.install', { tabIndex: 0 })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), installCount, localize('install count', "Install count"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), installCount, localize('install count', "Install count"))); const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCount, false); const rating = append(append(subtitle, $('.subtitle-entry')), $('span.rating.clickable', { tabIndex: 0 })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), rating, localize('rating', "Rating"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), rating, localize('rating', "Rating"))); rating.setAttribute('role', 'link'); // #132645 const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, rating, false); @@ -333,8 +333,7 @@ export class ExtensionEditor extends EditorPane { const actions = [ this.instantiationService.createInstance(ExtensionRuntimeStateAction), this.instantiationService.createInstance(ExtensionStatusLabelAction), - this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.updateActions', '', - [[this.instantiationService.createInstance(UpdateAction, true)], [this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction, true, [true, 'onlyEnabledExtensions'])]]), + this.instantiationService.createInstance(UpdateAction, true), this.instantiationService.createInstance(SetColorThemeAction), this.instantiationService.createInstance(SetFileIconThemeAction), this.instantiationService.createInstance(SetProductIconThemeAction), @@ -348,26 +347,35 @@ export class ExtensionEditor extends EditorPane { this.instantiationService.createInstance(WebInstallAction), installAction, this.instantiationService.createInstance(InstallingLabelAction), - this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.uninstall', UninstallAction.UninstallLabel, [ + this.instantiationService.createInstance(ButtonWithDropDownExtensionAction, 'extensions.uninstall', UninstallAction.UninstallClass, [ [ this.instantiationService.createInstance(MigrateDeprecatedExtensionAction, false), this.instantiationService.createInstance(UninstallAction), - this.instantiationService.createInstance(InstallAnotherVersionAction), + this.instantiationService.createInstance(InstallAnotherVersionAction, null, true), ] ]), this.instantiationService.createInstance(TogglePreReleaseExtensionAction), - this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction, false, [false, 'onlySelectedExtensions']), + this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction), new ExtensionEditorManageExtensionAction(this.scopedContextKeyService || this.contextKeyService, this.instantiationService), ]; const actionsAndStatusContainer = append(details, $('.actions-status-container')); const extensionActionBar = this._register(new ActionBar(actionsAndStatusContainer, { actionViewItemProvider: (action: IAction, options) => { - if (action instanceof ExtensionDropDownAction) { + if (action instanceof DropDownExtensionAction) { return action.createActionViewItem(options); } - if (action instanceof ActionWithDropDownAction) { - return new ExtensionActionWithDropdownActionViewItem(action, { ...options, icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService); + if (action instanceof ButtonWithDropDownExtensionAction) { + return new ButtonWithDropdownExtensionActionViewItem( + action, + { + ...options, + icon: true, + label: true, + menuActionsOrProvider: { getActions: () => action.menuActions }, + menuActionClassNames: action.menuActionClassNames + }, + this.contextMenuService); } if (action instanceof ToggleAutoUpdateForExtensionAction) { return new CheckboxActionViewItem(undefined, action, { ...options, icon: true, label: true, checkboxStyles: defaultCheckboxStyles }); @@ -552,14 +560,14 @@ export class ExtensionEditor extends EditorPane { const workspaceFolder = this.contextService.getWorkspaceFolder(location); if (workspaceFolder && extension.isWorkspaceScoped) { template.resource.parentElement?.classList.add('clickable'); - this.transientDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), template.resource, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location))); + this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.resource, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location))); template.resource.textContent = localize('workspace extension', "Workspace Extension"); this.transientDisposables.add(onClick(template.resource, () => { this.viewsService.openView(EXPLORER_VIEW_ID, true).then(() => this.explorerService.select(location, true)); })); } else { template.resource.parentElement?.classList.remove('clickable'); - this.transientDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), template.resource, location.path)); + this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.resource, location.path)); template.resource.textContent = localize('local extension', "Local Extension"); } } @@ -774,7 +782,7 @@ export class ExtensionEditor extends EditorPane { return ''; } - const content = await renderMarkdownDocument(contents, this.extensionService, this.languageService, extension.type !== ExtensionType.System, false, token); + const content = await renderMarkdownDocument(contents, this.extensionService, this.languageService, { shouldSanitize: extension.type !== ExtensionType.System, token }); if (token?.isCancellationRequested) { return ''; } @@ -968,7 +976,7 @@ export class ExtensionEditor extends EditorPane { for (const [label, uri] of resources) { const resource = append(resourcesElement, $('a.resource', { tabindex: '0' }, label)); this.transientDisposables.add(onClick(resource, () => this.openerService.open(uri))); - this.transientDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), resource, uri.toString())); + this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), resource, uri.toString())); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts b/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts index 42680a4ba1f..794127b7f6a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts @@ -178,10 +178,10 @@ export class ExtensionFeaturesTab extends Themable { return; } - const splitView = new SplitView(this.domNode, { + const splitView = this._register(new SplitView(this.domNode, { orientation: Orientation.HORIZONTAL, proportionalLayout: true - }); + })); this.layoutParticipants.push({ layout: (height: number, width: number) => { splitView.el.style.height = `${height - 14}px`; @@ -190,7 +190,7 @@ export class ExtensionFeaturesTab extends Themable { }); const featuresListContainer = $('.features-list-container'); - const list = this.createFeaturesList(featuresListContainer); + const list = this._register(this.createFeaturesList(featuresListContainer)); list.splice(0, list.length, features); const featureViewContainer = $('.feature-view-container'); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index 4152bf2f351..1f510c15b17 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -14,6 +14,7 @@ import { isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -283,9 +284,18 @@ export class ExtensionRecommendationNotificationService extends Disposable imple const installExtensions = async (isMachineScoped: boolean) => { this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); onDidInstallRecommendedExtensions(extensions); + const galleryExtensions: IGalleryExtension[] = [], resourceExtensions: IExtension[] = []; + for (const extension of extensions) { + if (extension.gallery) { + galleryExtensions.push(extension.gallery); + } else if (extension.resourceExtension) { + resourceExtensions.push(extension); + } + } await Promises.settled([ Promises.settled(extensions.map(extension => this.extensionsWorkbenchService.open(extension, { pinned: true }))), - this.extensionManagementService.installGalleryExtensions(extensions.map(e => ({ extension: e.gallery!, options: { isMachineScoped } }))) + galleryExtensions.length ? this.extensionManagementService.installGalleryExtensions(galleryExtensions.map(e => ({ extension: e, options: { isMachineScoped } }))) : Promise.resolve(), + resourceExtensions.length ? Promise.allSettled(resourceExtensions.map(r => this.extensionsWorkbenchService.install(r))) : Promise.resolve() ]); }; choices.push({ diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 92a43009c52..697fafb73d8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -13,8 +13,8 @@ import { EnablementState, IExtensionManagementServerService, IWorkbenchExtension import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, IExtension, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, IExtension, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, DefaultViewsContext, ExtensionsSortByContext, SearchHasTextContext } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; @@ -49,7 +49,6 @@ import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/brow import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService'; -import { IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ResourceContextKey, WorkbenchStateContext } from 'vs/workbench/common/contextkeys'; @@ -61,11 +60,9 @@ import { ExtensionEnablementWorkspaceTrustTransitionParticipant } from 'vs/workb import { clearSearchResultsIcon, configureRecommendedIcon, extensionsViewIcon, filterIcon, installWorkspaceRecommendedIcon, refreshIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; import { Disposable, DisposableStore, IDisposable, isDisposable } from 'vs/base/common/lifecycle'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; -import { Promises } from 'vs/base/common/async'; import { EditorExtensions } from 'vs/workbench/common/editor'; import { WORKSPACE_TRUST_EXTENSION_SUPPORT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { ExtensionsCompletionItemsProvider } from 'vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider'; @@ -80,6 +77,8 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { CONTEXT_KEYBINDINGS_EDITOR } from 'vs/workbench/contrib/preferences/common/preferences'; import { DeprecatedExtensionsChecker } from 'vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker'; import { ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IConfigurationMigrationRegistry, Extensions as ConfigurationMigrationExtensions } from 'vs/workbench/common/configuration'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */); @@ -127,17 +126,15 @@ Registry.as(ConfigurationExtensions.Configuration) ...extensionsConfigurationNodeBase, properties: { 'extensions.autoUpdate': { - enum: [true, 'onlyEnabledExtensions', 'onlySelectedExtensions', false,], + enum: [true, 'onlyEnabledExtensions', false,], enumItemLabels: [ localize('all', "All Extensions"), localize('enabled', "Only Enabled Extensions"), - localize('selected', "Only Selected Extensions"), localize('none', "None"), ], enumDescriptions: [ - localize('extensions.autoUpdate.true', 'Download and install updates automatically for all extensions except for those updates are ignored.'), - localize('extensions.autoUpdate.enabled', 'Download and install updates automatically only for enabled extensions except for those updates are ignored. Disabled extensions are not updated automatically.'), - localize('extensions.autoUpdate.selected', 'Download and install updates automatically only for selected extensions.'), + localize('extensions.autoUpdate.true', 'Download and install updates automatically for all extensions.'), + localize('extensions.autoUpdate.enabled', 'Download and install updates automatically only for enabled extensions.'), localize('extensions.autoUpdate.false', 'Extensions are not automatically updated.'), ], description: localize('extensions.autoUpdate', "Controls the automatic update behavior of extensions. The updates are fetched from a Microsoft online service."), @@ -379,7 +376,7 @@ CommandsRegistry.registerCommand({ } } else { const vsix = URI.revive(arg); - await extensionsWorkbenchService.install(vsix, { installOnlyNewlyAddedFromExtensionPack: options?.installOnlyNewlyAddedFromExtensionPackVSIX }); + await extensionsWorkbenchService.install(vsix, { installOnlyNewlyAddedFromExtensionPack: options?.installOnlyNewlyAddedFromExtensionPackVSIX, installGivenVersion: true }); } } catch (e) { onUnexpectedError(e); @@ -634,57 +631,38 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi } }); - const autoUpdateExtensionsSubMenu = new MenuId('autoUpdateExtensionsSubMenu'); - MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { - submenu: autoUpdateExtensionsSubMenu, - title: localize('configure auto updating extensions', "Auto Update Extensions"), - when: ContextKeyExpr.and(ContextKeyExpr.equals('viewContainer', VIEWLET_ID), CONTEXT_HAS_GALLERY), - group: '1_updates', - order: 5, + const enableAutoUpdateWhenCondition = ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, false); + this.registerExtensionAction({ + id: 'workbench.extensions.action.enableAutoUpdate', + title: localize2('enableAutoUpdate', 'Enable Auto Update for All Extensions'), + category: ExtensionsLocalizedLabel, + precondition: enableAutoUpdateWhenCondition, + menu: [{ + id: MenuId.ViewContainerTitle, + order: 5, + group: '1_updates', + when: enableAutoUpdateWhenCondition + }, { + id: MenuId.CommandPalette, + }], + run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateValue(true) }); + const disableAutoUpdateWhenCondition = ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, false); this.registerExtensionAction({ - id: 'configureExtensionsAutoUpdate.all', - title: localize('configureExtensionsAutoUpdate.all', "All Extensions"), - toggled: ContextKeyExpr.and(ContextKeyExpr.has(`config.${AutoUpdateConfigurationKey}`), ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions'), ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'onlySelectedExtensions')), + id: 'workbench.extensions.action.disableAutoUpdate', + title: localize2('disableAutoUpdate', 'Disable Auto Update for All Extensions'), + precondition: disableAutoUpdateWhenCondition, + category: ExtensionsLocalizedLabel, menu: [{ - id: autoUpdateExtensionsSubMenu, - order: 1, + id: MenuId.ViewContainerTitle, + order: 5, + group: '1_updates', + when: disableAutoUpdateWhenCondition + }, { + id: MenuId.CommandPalette, }], - run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, true) - }); - - this.registerExtensionAction({ - id: 'configureExtensionsAutoUpdate.enabled', - title: localize('configureExtensionsAutoUpdate.enabled', "Enabled Extensions"), - toggled: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions'), - menu: [{ - id: autoUpdateExtensionsSubMenu, - order: 2, - }], - run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, 'onlyEnabledExtensions') - }); - - this.registerExtensionAction({ - id: 'configureExtensionsAutoUpdate.selected', - title: localize('configureExtensionsAutoUpdate.selected', "Selected Extensions"), - toggled: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'onlySelectedExtensions'), - menu: [{ - id: autoUpdateExtensionsSubMenu, - order: 2, - }], - run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, 'onlySelectedExtensions') - }); - - this.registerExtensionAction({ - id: 'configureExtensionsAutoUpdate.none', - title: localize('configureExtensionsAutoUpdate.none', "None"), - toggled: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, false), - menu: [{ - id: autoUpdateExtensionsSubMenu, - order: 3, - }], - run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, false) + run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateValue(false) }); this.registerExtensionAction({ @@ -723,24 +701,6 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi } }); - this.registerExtensionAction({ - id: 'workbench.extensions.action.disableAutoUpdate', - title: localize2('disableAutoUpdate', 'Disable Auto Update for All Extensions'), - category: ExtensionsLocalizedLabel, - f1: true, - precondition: CONTEXT_HAS_GALLERY, - run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, false) - }); - - this.registerExtensionAction({ - id: 'workbench.extensions.action.enableAutoUpdate', - title: localize2('enableAutoUpdate', 'Enable Auto Update for All Extensions'), - category: ExtensionsLocalizedLabel, - f1: true, - precondition: CONTEXT_HAS_GALLERY, - run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, true) - }); - this.registerExtensionAction({ id: 'workbench.extensions.action.enableAll', title: localize2('enableAll', 'Enable All Extensions'), @@ -853,29 +813,51 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi when: ContextKeyExpr.and(ResourceContextKey.Extension.isEqualTo('.vsix'), ContextKeyExpr.or(CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER)), }], run: async (accessor: ServicesAccessor, resources: URI[] | URI) => { - const extensionService = accessor.get(IExtensionService); const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); const hostService = accessor.get(IHostService); const notificationService = accessor.get(INotificationService); - const extensions = Array.isArray(resources) ? resources : [resources]; - await Promises.settled(extensions.map(async (vsix) => await extensionsWorkbenchService.install(vsix))) - .then(async (extensions) => { - for (const extension of extensions) { - const requireReload = !(extension.local && extensionService.canAddExtension(toExtensionDescription(extension.local))); - const message = requireReload ? localize('InstallVSIXAction.successReload', "Completed installing {0} extension from VSIX. Please reload Visual Studio Code to enable it.", extension.displayName || extension.name) - : localize('InstallVSIXAction.success', "Completed installing {0} extension from VSIX.", extension.displayName || extension.name); - const actions = requireReload ? [{ - label: localize('InstallVSIXAction.reloadNow', "Reload Now"), - run: () => hostService.reload() - }] : []; - notificationService.prompt( - Severity.Info, - message, - actions - ); - } - }); + const vsixs = Array.isArray(resources) ? resources : [resources]; + const result = await Promise.allSettled(vsixs.map(async (vsix) => await extensionsWorkbenchService.install(vsix, { installGivenVersion: true }))); + let error: Error | undefined, requireReload = false, requireRestart = false; + for (const r of result) { + if (r.status === 'rejected') { + error = new Error(r.reason); + break; + } + requireReload = requireReload || r.value.runtimeState?.action === ExtensionRuntimeActionType.ReloadWindow; + requireRestart = requireRestart || r.value.runtimeState?.action === ExtensionRuntimeActionType.RestartExtensions; + } + if (error) { + throw error; + } + if (requireReload) { + notificationService.prompt( + Severity.Info, + localize('InstallVSIXAction.successReload', "Completed installing extension from VSIX. Please reload Visual Studio Code to enable it."), + [{ + label: localize('InstallVSIXAction.reloadNow', "Reload Now"), + run: () => hostService.reload() + }] + ); + } + else if (requireRestart) { + notificationService.prompt( + Severity.Info, + localize('InstallVSIXAction.successRestart', "Completed installing extension from VSIX. Please restart extensions to enable it."), + [{ + label: localize('InstallVSIXAction.restartExtensions', "Restart Extensions"), + run: () => extensionsWorkbenchService.updateRunningExtensions() + }] + ); + } + else { + notificationService.prompt( + Severity.Info, + localize('InstallVSIXAction.successNoReload', "Completed installing extension."), + [] + ); + } } }); @@ -1363,18 +1345,23 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: ToggleAutoUpdateForExtensionAction.ID, title: ToggleAutoUpdateForExtensionAction.LABEL, category: ExtensionsLocalizedLabel, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions'), ContextKeyExpr.equals('isExtensionEnabled', true)), ContextKeyExpr.not('extensionDisallowInstall')), menu: { id: MenuId.ExtensionContext, group: UPDATE_ACTIONS_GROUP, order: 1, - when: ContextKeyExpr.and(ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.not('isBuiltinExtension'), ContextKeyExpr.or(ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'onlySelectedExtensions'), ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, false)),) + when: ContextKeyExpr.and( + ContextKeyExpr.not('inExtensionEditor'), + ContextKeyExpr.equals('extensionStatus', 'installed'), + ContextKeyExpr.not('isBuiltinExtension'), + ) }, run: async (accessor: ServicesAccessor, id: string) => { const instantiationService = accessor.get(IInstantiationService); const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); const extension = extensionWorkbenchService.local.find(e => areSameExtensions(e.identifier, { id })); if (extension) { - const action = instantiationService.createInstance(ToggleAutoUpdateForExtensionAction, false, []); + const action = instantiationService.createInstance(ToggleAutoUpdateForExtensionAction); action.extension = extension; return action.run(); } @@ -1385,11 +1372,12 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: ToggleAutoUpdatesForPublisherAction.ID, title: { value: ToggleAutoUpdatesForPublisherAction.LABEL, original: 'Auto Update (Publisher)' }, category: ExtensionsLocalizedLabel, + precondition: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, false), menu: { id: MenuId.ExtensionContext, group: UPDATE_ACTIONS_GROUP, order: 2, - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.not('isBuiltinExtension'), ContextKeyExpr.or(ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'onlySelectedExtensions'), ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, false)),) + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.not('isBuiltinExtension')) }, run: async (accessor: ServicesAccessor, id: string) => { const instantiationService = accessor.get(IInstantiationService); @@ -1466,6 +1454,72 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi } }); + this.registerExtensionAction({ + id: 'workbench.extensions.action.installAndDonotSync', + title: localize('install installAndDonotSync', "Install (Do not Sync)"), + menu: { + id: MenuId.ExtensionContext, + group: '0_install', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall'), CONTEXT_SYNC_ENABLEMENT), + order: 1 + }, + run: async (accessor: ServicesAccessor, extensionId: string) => { + const instantiationService = accessor.get(IInstantiationService); + const extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; + if (extension) { + const action = instantiationService.createInstance(InstallAction, { + isMachineScoped: true, + }); + action.extension = extension; + return action.run(); + } + } + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.installPrereleaseAndDonotSync', + title: localize('installPrereleaseAndDonotSync', "Install Pre-Release (Do not Sync)"), + menu: { + id: MenuId.ExtensionContext, + group: '0_install', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('extensionDisallowInstall'), CONTEXT_SYNC_ENABLEMENT), + order: 2 + }, + run: async (accessor: ServicesAccessor, extensionId: string) => { + const instantiationService = accessor.get(IInstantiationService); + const extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; + if (extension) { + const action = instantiationService.createInstance(InstallAction, { + isMachineScoped: true, + preRelease: true + }); + action.extension = extension; + return action.run(); + } + } + }); + + this.registerExtensionAction({ + id: InstallAnotherVersionAction.ID, + title: InstallAnotherVersionAction.LABEL, + menu: { + id: MenuId.ExtensionContext, + group: '0_install', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall')), + order: 3 + }, + run: async (accessor: ServicesAccessor, extensionId: string) => { + const instantiationService = accessor.get(IInstantiationService); + const extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; + if (extension) { + return instantiationService.createInstance(InstallAnotherVersionAction, extension, false).run(); + } + } + }); + this.registerExtensionAction({ id: 'workbench.extensions.action.copyExtension', title: localize2('workbench.extensions.action.copyExtension', 'Copy'), @@ -1534,8 +1588,9 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('isDefaultApplicationScopedExtension').negate(), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.equals('isWorkspaceScopedExtension', false)), order: 3 }, - run: async (accessor: ServicesAccessor, id: string) => { - const extension = this.extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier)); + run: async (accessor: ServicesAccessor, _: string, extensionArg: IExtensionArg) => { + const uriIdentityService = accessor.get(IUriIdentityService); + const extension = extensionArg.location ? this.extensionsWorkbenchService.installed.find(e => uriIdentityService.extUri.isEqual(e.local?.location, extensionArg.location)) : undefined; if (extension) { return this.extensionsWorkbenchService.toggleApplyExtensionToAllProfiles(extension); } @@ -1548,7 +1603,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '2_configure', - when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.equals('isWorkspaceScopedExtension', false)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.equals('isWorkspaceScopedExtension', false)), order: 4 }, run: async (accessor: ServicesAccessor, id: string) => { @@ -1754,3 +1809,14 @@ if (isWeb) { // Running Extensions registerAction2(ShowRuntimeExtensionsAction); + +Registry.as(ConfigurationMigrationExtensions.ConfigurationMigration) + .registerConfigurationMigrations([{ + key: AutoUpdateConfigurationKey, + migrateFn: (value, accessor) => { + if (value === 'onlySelectedExtensions') { + return { value: false }; + } + return []; + } + }]); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index a2f80109939..3f7e240ced1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -5,17 +5,17 @@ import 'vs/css!./media/extensionActions'; import { localize, localize2 } from 'vs/nls'; -import { IAction, Action, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { IAction, Action, Separator, SubmenuAction, IActionChangeEvent } from 'vs/base/common/actions'; import { Delayer, Promises, Throttler } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; import { Emitter, Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { disposeIfDisposable } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, ExtensionEditorTab, ExtensionRuntimeActionType, IExtensionArg, AutoUpdateConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension, getWorkspaceSupportTypeMessage, TargetPlatform, isApplicationScopedExtension } from 'vs/platform/extensions/common/extensions'; @@ -52,7 +52,6 @@ import { IActionViewItemOptions, ActionViewItem } from 'vs/base/browser/ui/actio import { EXTENSIONS_CONFIG, IExtensionsConfigContent } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import { getErrorMessage, isCancellationError } from 'vs/base/common/errors'; import { IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; -import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; import { ILogService } from 'vs/platform/log/common/log'; import { errorIcon, infoIcon, manageExtensionIcon, syncEnabledIcon, syncIgnoredIcon, trustIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; @@ -73,6 +72,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { Registry } from 'vs/platform/registry/common/platform'; import { IUpdateService } from 'vs/platform/update/common/update'; +import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; export class PromptExtensionInstallFailureAction extends Action { @@ -134,7 +134,7 @@ export class PromptExtensionInstallFailureAction extends Action { return; } - if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious, ExtensionManagementErrorCode.Deprecated].includes(this.error.name)) { + if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleApi, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious, ExtensionManagementErrorCode.Deprecated].includes(this.error.name)) { await this.dialogService.info(getErrorMessage(this.error)); return; } @@ -216,21 +216,52 @@ export class PromptExtensionInstallFailureAction extends Action { } +export interface IExtensionActionChangeEvent extends IActionChangeEvent { + readonly hidden?: boolean; + readonly menuActions?: IAction[]; +} + export abstract class ExtensionAction extends Action implements IExtensionContainer { + + protected override _onDidChange = this._register(new Emitter()); + override readonly onDidChange = this._onDidChange.event; + static readonly EXTENSION_ACTION_CLASS = 'extension-action'; static readonly TEXT_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} text`; static readonly LABEL_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} label`; + static readonly PROMINENT_LABEL_ACTION_CLASS = `${ExtensionAction.LABEL_ACTION_CLASS} prominent`; static readonly ICON_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} icon`; + private _extension: IExtension | null = null; get extension(): IExtension | null { return this._extension; } set extension(extension: IExtension | null) { this._extension = extension; this.update(); } + + private _hidden: boolean = false; + get hidden(): boolean { return this._hidden; } + set hidden(hidden: boolean) { + if (this._hidden !== hidden) { + this._hidden = hidden; + this._onDidChange.fire({ hidden }); + } + } + + protected override _setEnabled(value: boolean): void { + super._setEnabled(value); + if (this.hideOnDisabled) { + this.hidden = !value; + } + } + + protected hideOnDisabled: boolean = true; + abstract update(): void; } -export class ActionWithDropDownAction extends ExtensionAction { +export class ButtonWithDropDownExtensionAction extends ExtensionAction { - private action: IAction | undefined; + private primaryAction: IAction | undefined; + readonly menuActionClassNames: string[] = []; private _menuActions: IAction[] = []; get menuActions(): IAction[] { return [...this._menuActions]; } @@ -246,10 +277,14 @@ export class ActionWithDropDownAction extends ExtensionAction { protected readonly extensionActions: ExtensionAction[]; constructor( - id: string, label: string, + id: string, + clazz: string, private readonly actionsGroups: ExtensionAction[][], ) { - super(id, label); + clazz = `${clazz} action-dropdown`; + super(id, undefined, clazz); + this.menuActionClassNames = clazz.split(' '); + this.hideOnDisabled = false; this.extensionActions = actionsGroups.flat(); this.update(); this._register(Event.any(...this.extensionActions.map(a => a.onDidChange))(() => this.update(true))); @@ -261,36 +296,35 @@ export class ActionWithDropDownAction extends ExtensionAction { this.extensionActions.forEach(a => a.update()); } - const enabledActionsGroups = this.actionsGroups.map(actionsGroup => actionsGroup.filter(a => a.enabled)); + const actionsGroups = this.actionsGroups.map(actionsGroup => actionsGroup.filter(a => !a.hidden)); let actions: IAction[] = []; - for (const enabledActions of enabledActionsGroups) { - if (enabledActions.length) { - actions = [...actions, ...enabledActions, new Separator()]; + for (const visibleActions of actionsGroups) { + if (visibleActions.length) { + actions = [...actions, ...visibleActions, new Separator()]; } } actions = actions.length ? actions.slice(0, actions.length - 1) : actions; - this.action = actions[0]; + this.primaryAction = actions[0]; this._menuActions = actions.length > 1 ? actions : []; + this._onDidChange.fire({ menuActions: this._menuActions }); - this.enabled = !!this.action; - if (this.action) { - this.label = this.getLabel(this.action as ExtensionAction); - this.tooltip = this.action.tooltip; + if (this.primaryAction) { + this.hidden = false; + this.enabled = this.primaryAction.enabled; + this.label = this.getLabel(this.primaryAction as ExtensionAction); + this.tooltip = this.primaryAction.tooltip; + } else { + this.hidden = true; + this.enabled = false; } - - let clazz = (this.action || this.extensionActions[0])?.class || ''; - clazz = clazz ? `${clazz} action-dropdown` : 'action-dropdown'; - if (this._menuActions.length === 0) { - clazz += ' action-dropdown'; - } - this.class = clazz; } - override run(): Promise { - const enabledActions = this.extensionActions.filter(a => a.enabled); - return enabledActions[0].run(); + override async run(): Promise { + if (this.enabled) { + await this.primaryAction?.run(); + } } protected getLabel(action: ExtensionAction): string { @@ -298,9 +332,42 @@ export class ActionWithDropDownAction extends ExtensionAction { } } +export class ButtonWithDropdownExtensionActionViewItem extends ActionWithDropdownActionViewItem { + + constructor( + action: ButtonWithDropDownExtensionAction, + options: IActionViewItemOptions & IActionWithDropdownActionViewItemOptions, + contextMenuProvider: IContextMenuProvider + ) { + super(null, action, options, contextMenuProvider); + this._register(action.onDidChange(e => { + if (e.hidden !== undefined || e.menuActions !== undefined) { + this.updateClass(); + } + })); + } + + override render(container: HTMLElement): void { + super.render(container); + this.updateClass(); + } + + protected override updateClass(): void { + super.updateClass(); + if (this.element && this.dropdownMenuActionViewItem?.element) { + this.element.classList.toggle('hide', (this._action).hidden); + const isMenuEmpty = (this._action).menuActions.length === 0; + this.element.classList.toggle('empty', isMenuEmpty); + this.dropdownMenuActionViewItem.element.classList.toggle('hide', isMenuEmpty); + } + } + +} + export class InstallAction extends ExtensionAction { - static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install`; + static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent install`; + private static readonly HIDE = `${this.CLASS} hide`; protected _manifest: IExtensionManifest | null = null; set manifest(manifest: IExtensionManifest | null) { @@ -323,8 +390,9 @@ export class InstallAction extends ExtensionAction { @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, ) { - super('extensions.install', localize('install', "Install"), InstallAction.Class, false); - this.options = { ...options, isMachineScoped: false }; + super('extensions.install', localize('install', "Install"), InstallAction.CLASS, false); + this.hideOnDisabled = false; + this.options = { isMachineScoped: false, ...options }; this.update(); this._register(this.labelService.onDidChangeFormatters(() => this.updateLabel(), this)); } @@ -335,6 +403,8 @@ export class InstallAction extends ExtensionAction { protected async computeAndUpdateEnablement(): Promise { this.enabled = false; + this.class = InstallAction.HIDE; + this.hidden = true; if (!this.extension) { return; } @@ -344,8 +414,19 @@ export class InstallAction extends ExtensionAction { if (this.extensionsWorkbenchService.canSetLanguage(this.extension)) { return; } - if (this.extension.state === ExtensionState.Uninstalled && await this.extensionsWorkbenchService.canInstall(this.extension)) { - this.enabled = this.options.installPreReleaseVersion ? this.extension.hasPreReleaseVersion : this.extension.hasReleaseVersion; + if (this.extension.state !== ExtensionState.Uninstalled) { + return; + } + if (this.options.installPreReleaseVersion && !this.extension.hasPreReleaseVersion) { + return; + } + if (!this.options.installPreReleaseVersion && !this.extension.hasReleaseVersion) { + return; + } + this.hidden = false; + this.class = InstallAction.CLASS; + if (await this.extensionsWorkbenchService.canInstall(this.extension)) { + this.enabled = true; this.updateLabel(); } } @@ -518,7 +599,7 @@ export class InstallAction extends ExtensionAction { } -export class InstallDropdownAction extends ActionWithDropDownAction { +export class InstallDropdownAction extends ButtonWithDropDownExtensionAction { set manifest(manifest: IExtensionManifest | null) { this.extensionActions.forEach(a => (a).manifest = manifest); @@ -529,7 +610,7 @@ export class InstallDropdownAction extends ActionWithDropDownAction { @IInstantiationService instantiationService: IInstantiationService, @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, ) { - super(`extensions.installActions`, '', [ + super(`extensions.installActions`, InstallAction.CLASS, [ [ instantiationService.createInstance(InstallAction, { installPreReleaseVersion: extensionsWorkbenchService.preferPreReleases }), instantiationService.createInstance(InstallAction, { installPreReleaseVersion: !extensionsWorkbenchService.preferPreReleases }), @@ -562,8 +643,8 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { protected static readonly INSTALL_LABEL = localize('install', "Install"); protected static readonly INSTALLING_LABEL = localize('installing', "Installing"); - private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install`; - private static readonly InstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} install installing`; + private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install-other-server`; + private static readonly InstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} install-other-server installing`; updateWhenCounterExtensionChanges: boolean = true; @@ -721,7 +802,7 @@ export class UninstallAction extends ExtensionAction { static readonly UninstallLabel = localize('uninstallAction', "Uninstall"); private static readonly UninstallingLabel = localize('Uninstalling', "Uninstalling"); - private static readonly UninstallClass = `${ExtensionAction.LABEL_ACTION_CLASS} uninstall`; + static readonly UninstallClass = `${ExtensionAction.LABEL_ACTION_CLASS} uninstall`; private static readonly UnInstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} uninstall uninstalling`; constructor( @@ -781,8 +862,8 @@ export class UninstallAction extends ExtensionAction { abstract class AbstractUpdateAction extends ExtensionAction { - private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} prominent update`; - private static readonly DisabledClass = `${AbstractUpdateAction.EnabledClass} disabled`; + private static readonly EnabledClass = `${this.LABEL_ACTION_CLASS} prominent update`; + private static readonly DisabledClass = `${this.EnabledClass} disabled`; private readonly updateThrottler = new Throttler(); @@ -859,14 +940,12 @@ export class ToggleAutoUpdateForExtensionAction extends ExtensionAction { static readonly LABEL = localize2('enableAutoUpdateLabel', "Auto Update"); private static readonly EnabledClass = `${ExtensionAction.EXTENSION_ACTION_CLASS} auto-update`; - private static readonly DisabledClass = `${ToggleAutoUpdateForExtensionAction.EnabledClass} hide`; + private static readonly DisabledClass = `${this.EnabledClass} hide`; constructor( - private readonly enableWhenOutdated: boolean, - private readonly enableWhenAutoUpdateValue: AutoUpdateConfigurationValue[], @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IConfigurationService configurationService: IConfigurationService, - ) { super(ToggleAutoUpdateForExtensionAction.ID, ToggleAutoUpdateForExtensionAction.LABEL.value, ToggleAutoUpdateForExtensionAction.DisabledClass); this._register(configurationService.onDidChangeConfiguration(e => { @@ -886,10 +965,10 @@ export class ToggleAutoUpdateForExtensionAction extends ExtensionAction { if (this.extension.isBuiltin) { return; } - if (this.enableWhenOutdated && (this.extension.state !== ExtensionState.Installed || !this.extension.outdated)) { + if (this.extension.deprecationInfo?.disallowInstall) { return; } - if (!this.enableWhenAutoUpdateValue.includes(this.extensionsWorkbenchService.getAutoUpdateValue())) { + if (this.extensionsWorkbenchService.getAutoUpdateValue() === 'onlyEnabledExtensions' && !this.extensionEnablementService.isEnabledEnablementState(this.extension.enablementState)) { return; } this.enabled = true; @@ -944,7 +1023,7 @@ export class ToggleAutoUpdatesForPublisherAction extends ExtensionAction { export class MigrateDeprecatedExtensionAction extends ExtensionAction { private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} migrate`; - private static readonly DisabledClass = `${MigrateDeprecatedExtensionAction.EnabledClass} disabled`; + private static readonly DisabledClass = `${this.EnabledClass} disabled`; constructor( private readonly small: boolean, @@ -987,32 +1066,7 @@ export class MigrateDeprecatedExtensionAction extends ExtensionAction { } } -export class ExtensionActionWithDropdownActionViewItem extends ActionWithDropdownActionViewItem { - - constructor( - action: ActionWithDropDownAction, - options: IActionViewItemOptions & IActionWithDropdownActionViewItemOptions, - contextMenuProvider: IContextMenuProvider - ) { - super(null, action, options, contextMenuProvider); - } - - override render(container: HTMLElement): void { - super.render(container); - this.updateClass(); - } - - protected override updateClass(): void { - super.updateClass(); - if (this.element && this.dropdownMenuActionViewItem && this.dropdownMenuActionViewItem.element) { - this.element.classList.toggle('empty', (this._action).menuActions.length === 0); - this.dropdownMenuActionViewItem.element.classList.toggle('hide', (this._action).menuActions.length === 0); - } - } - -} - -export abstract class ExtensionDropDownAction extends ExtensionAction { +export abstract class DropDownExtensionAction extends ExtensionAction { constructor( id: string, @@ -1024,9 +1078,9 @@ export abstract class ExtensionDropDownAction extends ExtensionAction { super(id, label, cssClass, enabled); } - private _actionViewItem: DropDownMenuActionViewItem | null = null; - createActionViewItem(options: IActionViewItemOptions): DropDownMenuActionViewItem { - this._actionViewItem = this.instantiationService.createInstance(DropDownMenuActionViewItem, this, options); + private _actionViewItem: DropDownExtensionActionViewItem | null = null; + createActionViewItem(options: IActionViewItemOptions): DropDownExtensionActionViewItem { + this._actionViewItem = this.instantiationService.createInstance(DropDownExtensionActionViewItem, this, options); return this._actionViewItem; } @@ -1036,10 +1090,10 @@ export abstract class ExtensionDropDownAction extends ExtensionAction { } } -export class DropDownMenuActionViewItem extends ActionViewItem { +export class DropDownExtensionActionViewItem extends ActionViewItem { constructor( - action: ExtensionDropDownAction, + action: DropDownExtensionAction, options: IActionViewItemOptions, @IContextMenuService private readonly contextMenuService: IContextMenuService ) { @@ -1072,6 +1126,7 @@ export class DropDownMenuActionViewItem extends ActionViewItem { async function getContextMenuActionsGroups(extension: IExtension | undefined | null, contextKeyService: IContextKeyService, instantiationService: IInstantiationService): Promise<[string, Array][]> { return instantiationService.invokeFunction(async accessor => { const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const extensionEnablementService = accessor.get(IWorkbenchExtensionEnablementService); const menuService = accessor.get(IMenuService); const extensionRecommendationsService = accessor.get(IExtensionRecommendationsService); const extensionIgnoredRecommendationsService = accessor.get(IExtensionIgnoredRecommendationsService); @@ -1084,6 +1139,7 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['isDefaultApplicationScopedExtension', extension.local && isApplicationScopedExtension(extension.local.manifest)]); cksOverlay.push(['isApplicationScopedExtension', extension.local && extension.local.isApplicationScoped]); cksOverlay.push(['isWorkspaceScopedExtension', extension.isWorkspaceScoped]); + cksOverlay.push(['isGalleryExtension', !!extension.identifier.uuid]); if (extension.local) { cksOverlay.push(['extensionSource', extension.local.source]); } @@ -1093,14 +1149,29 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['isExtensionRecommended', !!extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]]); cksOverlay.push(['isExtensionWorkspaceRecommended', extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]?.reasonId === ExtensionRecommendationReason.Workspace]); cksOverlay.push(['isUserIgnoredRecommendation', extensionIgnoredRecommendationsService.globalIgnoredRecommendations.some(e => e === extension.identifier.id.toLowerCase())]); - if (extension.state === ExtensionState.Installed) { - cksOverlay.push(['extensionStatus', 'installed']); + cksOverlay.push(['isExtensionPinned', extension.pinned]); + cksOverlay.push(['isExtensionEnabled', extensionEnablementService.isEnabledEnablementState(extension.enablementState)]); + switch (extension.state) { + case ExtensionState.Installing: + cksOverlay.push(['extensionStatus', 'installing']); + break; + case ExtensionState.Installed: + cksOverlay.push(['extensionStatus', 'installed']); + break; + case ExtensionState.Uninstalling: + cksOverlay.push(['extensionStatus', 'uninstalling']); + break; + case ExtensionState.Uninstalled: + cksOverlay.push(['extensionStatus', 'uninstalled']); + break; } cksOverlay.push(['installedExtensionIsPreReleaseVersion', !!extension.local?.isPreReleaseVersion]); cksOverlay.push(['installedExtensionIsOptedToPreRelease', !!extension.local?.preRelease]); cksOverlay.push(['galleryExtensionIsPreReleaseVersion', !!extension.gallery?.properties.isPreReleaseVersion]); cksOverlay.push(['galleryExtensionHasPreReleaseVersion', extension.gallery?.hasPreReleaseVersion]); + cksOverlay.push(['extensionHasPreReleaseVersion', extension.hasPreReleaseVersion]); cksOverlay.push(['extensionHasReleaseVersion', extension.hasReleaseVersion]); + cksOverlay.push(['extensionDisallowInstall', !!extension.deprecationInfo?.disallowInstall]); const [colorThemes, fileIconThemes, productIconThemes] = await Promise.all([workbenchThemeService.getColorThemes(), workbenchThemeService.getFileIconThemes(), workbenchThemeService.getProductIconThemes()]); cksOverlay.push(['extensionHasColorThemes', colorThemes.some(theme => isThemeFromExtension(theme, extension))]); @@ -1111,9 +1182,7 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['isActiveLanguagePackExtension', extension.gallery && language === getLocale(extension.gallery)]); } - const menu = menuService.createMenu(MenuId.ExtensionContext, contextKeyService.createOverlay(cksOverlay)); - const actionsGroups = menu.getActions({ shouldForwardArgs: true }); - menu.dispose(); + const actionsGroups = menuService.getMenuActions(MenuId.ExtensionContext, contextKeyService.createOverlay(cksOverlay), { shouldForwardArgs: true }); return actionsGroups; }); } @@ -1137,12 +1206,12 @@ export async function getContextMenuActions(extension: IExtension | undefined | return toActions(actionsGroups, instantiationService); } -export class ManageExtensionAction extends ExtensionDropDownAction { +export class ManageExtensionAction extends DropDownExtensionAction { static readonly ID = 'extensions.manage'; private static readonly Class = `${ExtensionAction.ICON_ACTION_CLASS} manage ` + ThemeIcon.asClassName(manageExtensionIcon); - private static readonly HideManageExtensionClass = `${ManageExtensionAction.Class} hide`; + private static readonly HideManageExtensionClass = `${this.Class} hide`; constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -1190,7 +1259,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction { } groups.push([ ...(installActions.length ? installActions : []), - this.instantiationService.createInstance(InstallAnotherVersionAction), + this.instantiationService.createInstance(InstallAnotherVersionAction, this.extension, false), this.instantiationService.createInstance(UninstallAction), ]); @@ -1221,7 +1290,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction { } } -export class ExtensionEditorManageExtensionAction extends ExtensionDropDownAction { +export class ExtensionEditorManageExtensionAction extends DropDownExtensionAction { constructor( private readonly contextKeyService: IContextKeyService, @@ -1255,6 +1324,14 @@ export class MenuItemExtensionAction extends ExtensionAction { super(action.id, action.label); } + override get enabled(): boolean { + return this.action.enabled; + } + + override set enabled(value: boolean) { + this.action.enabled = value; + } + update() { if (!this.extension) { return; @@ -1272,9 +1349,15 @@ export class MenuItemExtensionAction extends ExtensionAction { override async run(): Promise { if (this.extension) { - await this.action.run(this.extension.local ? getExtensionId(this.extension.local.manifest.publisher, this.extension.local.manifest.name) + const id = this.extension.local ? getExtensionId(this.extension.local.manifest.publisher, this.extension.local.manifest.name) : this.extension.gallery ? getExtensionId(this.extension.gallery.publisher, this.extension.gallery.name) - : this.extension.identifier.id); + : this.extension.identifier.id; + const extensionArg: IExtensionArg = { + id: this.extension.identifier.id, + version: this.extension.version, + location: this.extension.local?.location + }; + await this.action.run(id, extensionArg); } } } @@ -1285,7 +1368,7 @@ export class TogglePreReleaseExtensionAction extends ExtensionAction { static readonly LABEL = localize('togglePreRleaseLabel', "Pre-Release"); private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} pre-release`; - private static readonly DisabledClass = `${TogglePreReleaseExtensionAction.EnabledClass} hide`; + private static readonly DisabledClass = `${this.EnabledClass} hide`; constructor( @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @@ -1342,29 +1425,39 @@ export class TogglePreReleaseExtensionAction extends ExtensionAction { export class InstallAnotherVersionAction extends ExtensionAction { static readonly ID = 'workbench.extensions.action.install.anotherVersion'; - static readonly LABEL = localize('install another version', "Install Another Version..."); + static readonly LABEL = localize('install another version', "Install Specific Version..."); constructor( + extension: IExtension | null, + private readonly whenInstalled: boolean, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IDialogService private readonly dialogService: IDialogService, ) { super(InstallAnotherVersionAction.ID, InstallAnotherVersionAction.LABEL, ExtensionAction.LABEL_ACTION_CLASS); + this.extension = extension; this.update(); } update(): void { - this.enabled = !!this.extension && !this.extension.isBuiltin && !!this.extension.gallery && !!this.extension.local && !!this.extension.server && this.extension.state === ExtensionState.Installed && !this.extension.deprecationInfo; + this.enabled = !!this.extension && !this.extension.isBuiltin && !!this.extension.identifier.uuid && !this.extension.deprecationInfo; + if (this.enabled && this.whenInstalled) { + this.enabled = !!this.extension?.local && !!this.extension.server && this.extension.state === ExtensionState.Installed; + } } override async run(): Promise { if (!this.enabled) { return; } - const targetPlatform = await this.extension!.server!.extensionManagementService.getTargetPlatform(); - const allVersions = await this.extensionGalleryService.getAllCompatibleVersions(this.extension!.gallery!, this.extension!.local!.preRelease, targetPlatform); + if (!this.extension) { + return; + } + const targetPlatform = this.extension.server ? await this.extension.server.extensionManagementService.getTargetPlatform() : await this.extensionManagementService.getTargetPlatform(); + const allVersions = await this.extensionGalleryService.getAllCompatibleVersions(this.extension.identifier, this.extension.local?.preRelease ?? this.extension.gallery?.properties.isPreReleaseVersion ?? false, targetPlatform); if (!allVersions.length) { await this.dialogService.info(localize('no versions', "This extension has no other versions.")); return; @@ -1374,8 +1467,7 @@ export class InstallAnotherVersionAction extends ExtensionAction { return { id: v.version, label: v.version, - description: `${fromNow(new Date(Date.parse(v.date)), true)}${v.isPreReleaseVersion ? ` (${localize('pre-release', "pre-release")})` : ''}${v.version === this.extension!.version ? ` (${localize('current', "current")})` : ''}`, - latest: i === 0, + description: `${fromNow(new Date(Date.parse(v.date)), true)}${v.isPreReleaseVersion ? ` (${localize('pre-release', "pre-release")})` : ''}${v.version === this.extension?.local?.manifest.version ? ` (${localize('current', "current")})` : ''}`, ariaLabel: `${v.isPreReleaseVersion ? 'Pre-Release version' : 'Release version'} ${v.version}`, isPreReleaseVersion: v.isPreReleaseVersion }; @@ -1386,18 +1478,13 @@ export class InstallAnotherVersionAction extends ExtensionAction { matchOnDetail: true }); if (pick) { - if (this.extension!.version === pick.id) { + if (this.extension.local?.manifest.version === pick.id) { return; } try { - if (pick.latest) { - const [extension] = pick.id !== this.extension?.version ? await this.extensionsWorkbenchService.getExtensions([{ id: this.extension!.identifier.id, preRelease: pick.isPreReleaseVersion }], CancellationToken.None) : [this.extension]; - await this.extensionsWorkbenchService.install(extension ?? this.extension!, { installPreReleaseVersion: pick.isPreReleaseVersion }); - } else { - await this.extensionsWorkbenchService.install(this.extension!, { installPreReleaseVersion: pick.isPreReleaseVersion, version: pick.id }); - } + await this.extensionsWorkbenchService.install(this.extension, { installPreReleaseVersion: pick.isPreReleaseVersion, version: pick.id }); } catch (error) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension!, pick.latest ? this.extension!.latestVersion : pick.id, InstallOperation.Install, error).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension, pick.id, InstallOperation.Install, error).run(); } } return null; @@ -1534,12 +1621,12 @@ export class DisableGloballyAction extends ExtensionAction { } } -export class EnableDropDownAction extends ActionWithDropDownAction { +export class EnableDropDownAction extends ButtonWithDropDownExtensionAction { constructor( @IInstantiationService instantiationService: IInstantiationService ) { - super('extensions.enable', localize('enableAction', "Enable"), [ + super('extensions.enable', ExtensionAction.LABEL_ACTION_CLASS, [ [ instantiationService.createInstance(EnableGloballyAction), instantiationService.createInstance(EnableForWorkspaceAction) @@ -1548,12 +1635,12 @@ export class EnableDropDownAction extends ActionWithDropDownAction { } } -export class DisableDropDownAction extends ActionWithDropDownAction { +export class DisableDropDownAction extends ButtonWithDropDownExtensionAction { constructor( @IInstantiationService instantiationService: IInstantiationService ) { - super('extensions.disable', localize('disableAction', "Disable"), [[ + super('extensions.disable', ExtensionAction.LABEL_ACTION_CLASS, [[ instantiationService.createInstance(DisableGloballyAction), instantiationService.createInstance(DisableForWorkspaceAction) ]]); @@ -1564,7 +1651,7 @@ export class DisableDropDownAction extends ActionWithDropDownAction { export class ExtensionRuntimeStateAction extends ExtensionAction { private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} reload`; - private static readonly DisabledClass = `${ExtensionRuntimeStateAction.EnabledClass} disabled`; + private static readonly DisabledClass = `${this.EnabledClass} disabled`; updateWhenCounterExtensionChanges: boolean = true; @@ -1678,7 +1765,7 @@ export class SetColorThemeAction extends ExtensionAction { static readonly TITLE = localize2('workbench.extensions.action.setColorTheme', 'Set Color Theme'); private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} theme`; - private static readonly DisabledClass = `${SetColorThemeAction.EnabledClass} disabled`; + private static readonly DisabledClass = `${this.EnabledClass} disabled`; constructor( @IExtensionService extensionService: IExtensionService, @@ -1729,7 +1816,7 @@ export class SetFileIconThemeAction extends ExtensionAction { static readonly TITLE = localize2('workbench.extensions.action.setFileIconTheme', 'Set File Icon Theme'); private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} theme`; - private static readonly DisabledClass = `${SetFileIconThemeAction.EnabledClass} disabled`; + private static readonly DisabledClass = `${this.EnabledClass} disabled`; constructor( @IExtensionService extensionService: IExtensionService, @@ -1779,7 +1866,7 @@ export class SetProductIconThemeAction extends ExtensionAction { static readonly TITLE = localize2('workbench.extensions.action.setProductIconTheme', 'Set Product Icon Theme'); private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} theme`; - private static readonly DisabledClass = `${SetProductIconThemeAction.EnabledClass} disabled`; + private static readonly DisabledClass = `${this.EnabledClass} disabled`; constructor( @IExtensionService extensionService: IExtensionService, @@ -1830,7 +1917,7 @@ export class SetLanguageAction extends ExtensionAction { static readonly TITLE = localize2('workbench.extensions.action.setDisplayLanguage', 'Set Display Language'); private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} language`; - private static readonly DisabledClass = `${SetLanguageAction.EnabledClass} disabled`; + private static readonly DisabledClass = `${this.EnabledClass} disabled`; constructor( @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @@ -1866,7 +1953,7 @@ export class ClearLanguageAction extends ExtensionAction { static readonly TITLE = localize2('workbench.extensions.action.clearLanguage', 'Clear Display Language'); private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} language`; - private static readonly DisabledClass = `${ClearLanguageAction.EnabledClass} disabled`; + private static readonly DisabledClass = `${this.EnabledClass} disabled`; constructor( @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @@ -2178,7 +2265,7 @@ export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends Abstrac export class ExtensionStatusLabelAction extends Action implements IExtensionContainer { private static readonly ENABLED_CLASS = `${ExtensionAction.TEXT_ACTION_CLASS} extension-status-label`; - private static readonly DISABLED_CLASS = `${ExtensionStatusLabelAction.ENABLED_CLASS} hide`; + private static readonly DISABLED_CLASS = `${this.ENABLED_CLASS} hide`; private initialStatus: ExtensionState | null = null; private status: ExtensionState | null = null; @@ -2278,10 +2365,10 @@ export class ExtensionStatusLabelAction extends Action implements IExtensionCont } -export class ToggleSyncExtensionAction extends ExtensionDropDownAction { +export class ToggleSyncExtensionAction extends DropDownExtensionAction { private static readonly IGNORED_SYNC_CLASS = `${ExtensionAction.ICON_ACTION_CLASS} extension-sync ${ThemeIcon.asClassName(syncIgnoredIcon)}`; - private static readonly SYNC_CLASS = `${ToggleSyncExtensionAction.ICON_ACTION_CLASS} extension-sync ${ThemeIcon.asClassName(syncEnabledIcon)}`; + private static readonly SYNC_CLASS = `${this.ICON_ACTION_CLASS} extension-sync ${ThemeIcon.asClassName(syncEnabledIcon)}`; constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @@ -2571,7 +2658,6 @@ export class ExtensionStatusAction extends ExtensionAction { } } if (this.extension.enablementState === EnablementState.EnabledGlobally) { - this.updateStatus({ message: new MarkdownString(localize('globally enabled', "This extension is enabled globally.")) }, true); return; } } @@ -2718,16 +2804,14 @@ export class InstallSpecificVersionOfExtensionAction extends Action { override async run(): Promise { const extensionPick = await this.quickInputService.pick(this.getExtensionEntries(), { placeHolder: localize('selectExtension', "Select Extension"), matchOnDetail: true }); if (extensionPick && extensionPick.extension) { - const action = this.instantiationService.createInstance(InstallAnotherVersionAction); - action.extension = extensionPick.extension; + const action = this.instantiationService.createInstance(InstallAnotherVersionAction, extensionPick.extension, true); await action.run(); await this.instantiationService.createInstance(SearchExtensionsAction, extensionPick.extension.identifier.id).run(); } } private isEnabled(extension: IExtension): boolean { - const action = this.instantiationService.createInstance(InstallAnotherVersionAction); - action.extension = extension; + const action = this.instantiationService.createInstance(InstallAnotherVersionAction, extension, true); return action.enabled && !!extension.local && this.extensionEnablementService.isEnabled(extension.local); } @@ -3010,12 +3094,7 @@ registerColor('extensionButton.hoverBackground', { hcLight: null }, localize('extensionButtonHoverBackground', "Button background hover color for extension actions.")); -registerColor('extensionButton.separator', { - dark: buttonSeparator, - light: buttonSeparator, - hcDark: buttonSeparator, - hcLight: buttonSeparator -}, localize('extensionButtonSeparator', "Button separator color for extension actions")); +registerColor('extensionButton.separator', buttonSeparator, localize('extensionButtonSeparator', "Button separator color for extension actions")); export const extensionButtonProminentBackground = registerColor('extensionButton.prominentBackground', { dark: buttonBackground, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 683098f05b6..95c05e762e5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -13,13 +13,12 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ManageExtensionAction, ExtensionRuntimeStateAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction, ToggleAutoUpdateForExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { ManageExtensionAction, ExtensionRuntimeStateAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ButtonWithDropDownExtensionAction, InstallDropdownAction, InstallingLabelAction, ButtonWithDropdownExtensionActionViewItem, DropDownExtensionAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget, PreReleaseBookmarkWidget, extensionVerifiedPublisherIconColor, VerifiedPublisherWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; -import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; import { WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; @@ -69,8 +68,8 @@ export class Renderer implements IPagedRenderer { @IInstantiationService private readonly instantiationService: IInstantiationService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly extensionService: IExtensionService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { } @@ -100,10 +99,19 @@ export class Renderer implements IPagedRenderer { const publisherDisplayName = append(publisher, $('.publisher-name.ellipsis')); const actionbar = new ActionBar(footer, { actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { - if (action instanceof ActionWithDropDownAction) { - return new ExtensionActionWithDropdownActionViewItem(action, { ...options, icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService); + if (action instanceof ButtonWithDropDownExtensionAction) { + return new ButtonWithDropdownExtensionActionViewItem( + action, + { + ...options, + icon: true, + label: true, + menuActionsOrProvider: { getActions: () => action.menuActions }, + menuActionClassNames: action.menuActionClassNames + }, + this.contextMenuService); } - if (action instanceof ExtensionDropDownAction) { + if (action instanceof DropDownExtensionAction) { return action.createActionViewItem(options); } return undefined; @@ -118,8 +126,7 @@ export class Renderer implements IPagedRenderer { this.instantiationService.createInstance(ExtensionStatusLabelAction), this.instantiationService.createInstance(MigrateDeprecatedExtensionAction, true), this.instantiationService.createInstance(ExtensionRuntimeStateAction), - this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.updateActions', '', - [[this.instantiationService.createInstance(UpdateAction, false)], [this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction, true, [true, 'onlyEnabledExtensions'])]]), + this.instantiationService.createInstance(UpdateAction, false), this.instantiationService.createInstance(InstallDropdownAction), this.instantiationService.createInstance(InstallingLabelAction), this.instantiationService.createInstance(SetLanguageAction), @@ -185,23 +192,8 @@ export class Renderer implements IPagedRenderer { data.extensionDisposables = dispose(data.extensionDisposables); - const computeEnablement = async () => { - if (extension.state === ExtensionState.Uninstalled) { - if (!!extension.deprecationInfo) { - return true; - } - if (this.extensionsWorkbenchService.canSetLanguage(extension)) { - return false; - } - return !(await this.extensionsWorkbenchService.canInstall(extension)); - } else if (extension.local && !isLanguagePackExtension(extension.local.manifest)) { - const runningExtension = this.extensionService.extensions.filter(e => areSameExtensions({ id: e.identifier.value }, extension.identifier))[0]; - return !(runningExtension && extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); - } - return false; - }; - const updateEnablement = async () => { - const disabled = await computeEnablement(); + const updateEnablement = () => { + const disabled = extension.state === ExtensionState.Installed && extension.local && !this.extensionEnablementService.isEnabled(extension.local); const deprecated = !!extension.deprecationInfo; data.element.classList.toggle('deprecated', deprecated); data.root.classList.toggle('disabled', disabled); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 7ca65d956ce..9a81262ecf8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -16,7 +16,7 @@ import { append, $, Dimension, hide, show, DragAndDropObserver, trackFocus } fro import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu } from '../common/extensions'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, AutoRestartConfigurationKey } from '../common/extensions'; import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -846,7 +846,8 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution constructor( @IActivityService private readonly activityService: IActivityService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.onServiceChange(); @@ -856,7 +857,7 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution private onServiceChange(): void { this.badgeHandle.clear(); - const actionRequired = this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); + const actionRequired = this.configurationService.getValue(AutoRestartConfigurationKey) === true ? [] : this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0); const newBadgeNumber = outdated + actionRequired.length; if (newBadgeNumber > 0) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 4dcb55bb040..933788b162f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -41,7 +41,6 @@ import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from 'vs import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/browser/severityIcon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; @@ -214,9 +213,7 @@ export class ExtensionsListView extends ViewPane { return localize('extensions', "Extensions"); } }, - overrideStyles: { - listBackground: SIDE_BAR_BACKGROUND - }, + overrideStyles: this.getLocationBasedColors().listOverrideStyles, openOnSingleClick: true }) as WorkbenchPagedList; this._register(this.list.onContextMenu(e => this.onContextMenu(e), this)); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 3d725460a20..4b53d91cbc5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -42,7 +42,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -126,7 +126,7 @@ export class InstallCountWidget extends ExtensionWidget { export class RatingsWidget extends ExtensionWidget { - private readonly containerHover: IUpdatableHover; + private readonly containerHover: IManagedHover; constructor( private container: HTMLElement, @@ -140,7 +140,7 @@ export class RatingsWidget extends ExtensionWidget { container.classList.add('small'); } - this.containerHover = this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), container, '')); + this.containerHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, '')); this.render(); } @@ -192,7 +192,7 @@ export class RatingsWidget extends ExtensionWidget { export class VerifiedPublisherWidget extends ExtensionWidget { private readonly disposables = this._register(new DisposableStore()); - private readonly containerHover: IUpdatableHover; + private readonly containerHover: IManagedHover; constructor( private container: HTMLElement, @@ -201,7 +201,7 @@ export class VerifiedPublisherWidget extends ExtensionWidget { @IOpenerService private readonly openerService: IOpenerService, ) { super(); - this.containerHover = this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), container, '')); + this.containerHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, '')); this.render(); } @@ -258,7 +258,7 @@ export class SponsorWidget extends ExtensionWidget { } const sponsor = append(this.container, $('span.sponsor.clickable', { tabIndex: 0 })); - this.disposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), sponsor, this.extension?.publisherSponsorLink.toString() ?? '')); + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), sponsor, this.extension?.publisherSponsorLink.toString() ?? '')); sponsor.setAttribute('role', 'link'); // #132645 const sponsorIconElement = renderIcon(sponsorIcon); const label = $('span', undefined, localize('sponsor', "Sponsor")); @@ -294,9 +294,7 @@ export class RecommendationWidget extends ExtensionWidget { } private clear(): void { - if (this.element) { - this.parent.removeChild(this.element); - } + this.element?.remove(); this.element = undefined; this.disposables.clear(); } @@ -330,9 +328,7 @@ export class PreReleaseBookmarkWidget extends ExtensionWidget { } private clear(): void { - if (this.element) { - this.parent.removeChild(this.element); - } + this.element?.remove(); this.element = undefined; this.disposables.clear(); } @@ -367,9 +363,7 @@ export class RemoteBadgeWidget extends ExtensionWidget { } private clear(): void { - if (this.remoteBadge.value) { - this.element.removeChild(this.remoteBadge.value.element); - } + this.remoteBadge.value?.element.remove(); this.remoteBadge.clear(); } @@ -386,7 +380,7 @@ export class RemoteBadgeWidget extends ExtensionWidget { class RemoteBadge extends Disposable { readonly element: HTMLElement; - readonly elementHover: IUpdatableHover; + readonly elementHover: IManagedHover; constructor( private readonly tooltip: boolean, @@ -397,7 +391,7 @@ class RemoteBadge extends Disposable { ) { super(); this.element = $('div.extension-badge.extension-remote-badge'); - this.elementHover = this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.element, '')); + this.elementHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, '')); this.render(); } @@ -478,7 +472,7 @@ export class SyncIgnoredWidget extends ExtensionWidget { if (this.extension && this.extension.state === ExtensionState.Installed && this.userDataSyncEnablementService.isEnabled() && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)) { const element = append(this.container, $('span.extension-sync-ignored' + ThemeIcon.asCSSSelector(syncIgnoredIcon))); - this.disposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), element, localize('syncingore.label', "This extension is ignored during sync."))); + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, localize('syncingore.label', "This extension is ignored during sync."))); element.classList.add(...ThemeIcon.asClassNameArray(syncIgnoredIcon)); } } @@ -551,7 +545,7 @@ export class ExtensionHoverWidget extends ExtensionWidget { render(): void { this.hover.value = undefined; if (this.extension) { - this.hover.value = this.hoverService.setupUpdatableHover({ + this.hover.value = this.hoverService.setupManagedHover({ delay: this.configurationService.getValue('workbench.hover.delay'), showHover: (options, focus) => { return this.hoverService.showHover({ @@ -830,7 +824,7 @@ export class ExtensionRecommendationWidget extends ExtensionWidget { } export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hcDark: '#FF8E00', hcLight: textLinkForeground }, localize('extensionIconStarForeground', "The icon color for extension ratings."), true); -export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', { dark: textLinkForeground, light: textLinkForeground, hcDark: textLinkForeground, hcLight: textLinkForeground }, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), true); +export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', textLinkForeground, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), true); export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hcDark: '#1d9271', hcLight: textLinkForeground }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), true); export const extensionSponsorIconColor = registerColor('extensionIcon.sponsorForeground', { light: '#B51E78', dark: '#D758B3', hcDark: null, hcLight: '#B51E78' }, localize('extensionIcon.sponsorForeground', "The icon color for extension sponsor."), true); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index ff6e3ac39d7..78abd2bffab 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { firstOrDefault, index } from 'vs/base/common/arrays'; import { CancelablePromise, Promises, ThrottledDelayer, createCancelablePromise } from 'vs/base/common/async'; import { CancellationError, isCancellationError } from 'vs/base/common/errors'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { @@ -18,19 +18,19 @@ import { IExtensionsControlManifest, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX, InstallOptions, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension, extensionsConfigurationNodeBase } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType, AutoRestartConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url'; import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ILogService } from 'vs/platform/log/common/log'; import { IProgressOptions, IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, NotificationPriority, Severity } from 'vs/platform/notification/common/notification'; import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -58,6 +58,8 @@ import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator' import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; interface IExtensionStateProvider { (extension: Extension): T; @@ -339,8 +341,9 @@ export class Extension implements IExtension { return !!this.gallery?.properties.isPreReleaseVersion; } + private _extensionEnabledWithPreRelease: boolean | undefined; get hasPreReleaseVersion(): boolean { - return !!this.gallery?.hasPreReleaseVersion || !!this.local?.hasPreReleaseVersion; + return !!this.gallery?.hasPreReleaseVersion || !!this.local?.hasPreReleaseVersion || !!this._extensionEnabledWithPreRelease; } get hasReleaseVersion(): boolean { @@ -498,6 +501,12 @@ ${this.description} return []; } + setExtensionsControlManifest(extensionsControlManifest: IExtensionsControlManifest): void { + this.isMalicious = extensionsControlManifest.malicious.some(identifier => areSameExtensions(this.identifier, identifier)); + this.deprecationInfo = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[this.identifier.id.toLowerCase()] : undefined; + this._extensionEnabledWithPreRelease = extensionsControlManifest?.extensionsEnabledWithPreRelease?.includes(this.identifier.id.toLowerCase()); + } + private getManifestFromLocalOrResource(): IExtensionManifest | null { if (this.local) { return this.local.manifest; @@ -510,14 +519,10 @@ ${this.description} } const EXTENSIONS_AUTO_UPDATE_KEY = 'extensions.autoUpdate'; +const EXTENSIONS_DONOT_AUTO_UPDATE_KEY = 'extensions.donotAutoUpdate'; class Extensions extends Disposable { - static updateExtensionFromControlManifest(extension: Extension, extensionsControlManifest: IExtensionsControlManifest): void { - extension.isMalicious = extensionsControlManifest.malicious.some(identifier => areSameExtensions(extension.identifier, identifier)); - extension.deprecationInfo = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[extension.identifier.id.toLowerCase()] : undefined; - } - private readonly _onChange = this._register(new Emitter<{ extension: Extension; operation?: InstallOperation } | undefined>()); get onChange() { return this._onChange.event; } @@ -536,6 +541,7 @@ class Extensions extends Disposable { @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IWorkbenchExtensionManagementService private readonly workbenchExtensionManagementService: IWorkbenchExtensionManagementService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { @@ -544,7 +550,7 @@ class Extensions extends Disposable { this._register(server.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e))); this._register(server.extensionManagementService.onUninstallExtension(e => this.onUninstallExtension(e.identifier))); this._register(server.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e))); - this._register(server.extensionManagementService.onDidUpdateExtensionMetadata(e => this.onDidUpdateExtensionMetadata(e))); + this._register(server.extensionManagementService.onDidUpdateExtensionMetadata(e => this.onDidUpdateExtensionMetadata(e.local))); this._register(server.extensionManagementService.onDidChangeProfile(() => this.reset())); this._register(extensionEnablementService.onEnablementChanged(e => this.onEnablementChanged(e))); this._register(Event.any(this.onChange, this.onReset)(() => this._local = undefined)); @@ -666,7 +672,7 @@ class Extensions extends Disposable { const galleryWithLocalVersion: IGalleryExtension | undefined = (await this.galleryService.getExtensions([{ ...localExtension.identifier, version: localExtension.manifest.version }], CancellationToken.None))[0]; isPreReleaseVersion = !!galleryWithLocalVersion?.properties?.isPreReleaseVersion; } - return this.server.extensionManagementService.updateMetadata(localExtension, { id: gallery.identifier.uuid, publisherDisplayName: gallery.publisherDisplayName, publisherId: gallery.publisherId, isPreReleaseVersion }); + return this.server.extensionManagementService.updateMetadata(localExtension, { id: gallery.identifier.uuid, publisherDisplayName: gallery.publisherDisplayName, publisherId: gallery.publisherId, isPreReleaseVersion }, this.userDataProfileService.currentProfile.extensionsResource); } canInstall(galleryExtension: IGalleryExtension): Promise { @@ -690,11 +696,28 @@ class Extensions extends Disposable { all.push(...await this.workbenchExtensionManagementService.getInstalledWorkspaceExtensions(true)); } - // dedup user and system extensions by giving priority to user extensions. + // dedup workspace, user and system extensions by giving priority to workspace first and then to user extension. const installed = groupByExtension(all, r => r.identifier).reduce((result, extensions) => { - const extension = extensions.length === 1 ? extensions[0] - : extensions.find(e => e.type === ExtensionType.User) || extensions.find(e => e.type === ExtensionType.System); - result.push(extension!); + if (extensions.length === 1) { + result.push(extensions[0]); + } else { + let workspaceExtension: ILocalExtension | undefined, + userExtension: ILocalExtension | undefined, + systemExtension: ILocalExtension | undefined; + for (const extension of extensions) { + if (extension.isWorkspaceScoped) { + workspaceExtension = extension; + } else if (extension.type === ExtensionType.User) { + userExtension = extension; + } else { + systemExtension = extension; + } + } + const extension = workspaceExtension ?? userExtension ?? systemExtension; + if (extension) { + result.push(extension); + } + } return result; }, []); @@ -703,7 +726,7 @@ class Extensions extends Disposable { const extension = byId[local.identifier.id] || this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined, undefined); extension.local = local; extension.enablementState = this.extensionEnablementService.getEnablementState(local); - Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); + extension.setExtensionsControlManifest(extensionsControlManifest); return extension; }); } @@ -739,7 +762,7 @@ class Extensions extends Disposable { if (!extension.gallery) { extension.gallery = gallery; } - Extensions.updateExtensionFromControlManifest(extension, await this.server.extensionManagementService.getExtensionsControlManifest()); + extension.setExtensionsControlManifest(await this.server.extensionManagementService.getExtensionsControlManifest()); extension.enablementState = this.extensionEnablementService.getEnablementState(local); } } @@ -932,9 +955,27 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension urlService.registerHandler(this); + if (this.productService.quality !== 'stable') { + this.registerAutoRestartConfig(); + } + this.whenInitialized = this.initialize(); } + private registerAutoRestartConfig(): void { + Registry.as(ConfigurationExtensions.Configuration) + .registerConfiguration({ + ...extensionsConfigurationNodeBase, + properties: { + [AutoRestartConfigurationKey]: { + type: 'boolean', + description: nls.localize('autoRestart', "If activated, extensions will automatically restart following an update if the window is not in focus. There can be a data loss if you have open Notebooks or Custom Editors."), + default: false, + } + } + }); + } + private async initialize(): Promise { // initialize local extensions await Promise.all([this.queryLocal(), this.extensionService.whenInstalledExtensionsRegistered()]); @@ -951,14 +992,29 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.initializeAutoUpdate(); this.reportInstalledExtensionsTelemetry(); this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.reportProgressFromOtherSources())); - this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, EXTENSIONS_AUTO_UPDATE_KEY, this._store)(e => this.onDidSelectedExtensionToAutoUpdateValueChange(false))); + this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, EXTENSIONS_AUTO_UPDATE_KEY, this._store)(e => this.onDidSelectedExtensionToAutoUpdateValueChange())); + this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, EXTENSIONS_DONOT_AUTO_UPDATE_KEY, this._store)(e => this.onDidSelectedExtensionToAutoUpdateValueChange())); } private initializeAutoUpdate(): void { + // Initialise Auto Update Value + let autoUpdateValue = this.getAutoUpdateValue(); + // Register listeners for auto updates this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { - this.onDidAutoUpdateConfigurationChange(); + const wasAutoUpdateEnabled = autoUpdateValue !== false; + autoUpdateValue = this.getAutoUpdateValue(); + const isAutoUpdateEnabled = this.isAutoUpdateEnabled(); + if (wasAutoUpdateEnabled !== isAutoUpdateEnabled) { + this.setEnabledAutoUpdateExtensions([]); + this.setDisabledAutoUpdateExtensions([]); + this._onChange.fire(undefined); + this.extensionManagementService.resetPinnedStateForAllUserExtensions(!isAutoUpdateEnabled); + } + if (isAutoUpdateEnabled) { + this.eventuallyAutoUpdateExtensions(); + } } if (e.affectsConfiguration(AutoCheckUpdatesConfigurationKey)) { if (this.isAutoCheckUpdatesEnabled()) { @@ -973,9 +1029,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension })); this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0))); this._register(this.updateService.onStateChange(e => { - if (!this.isAutoUpdateEnabled()) { - return; - } if ((e.type === StateType.CheckingForUpdates && e.explicit) || e.type === StateType.AvailableForDownload || e.type === StateType.Downloaded) { this.eventuallyCheckForUpdates(true); } @@ -994,6 +1047,55 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.autoUpdateBuiltinExtensions(); } } + + this.registerAutoRestartListener(); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AutoRestartConfigurationKey)) { + this.registerAutoRestartListener(); + } + })); + } + + private isAutoUpdateEnabled(): boolean { + return this.getAutoUpdateValue() !== false; + } + + getAutoUpdateValue(): AutoUpdateConfigurationValue { + const autoUpdate = this.configurationService.getValue(AutoUpdateConfigurationKey); + if (autoUpdate === 'onlySelectedExtensions') { + return false; + } + return isBoolean(autoUpdate) || autoUpdate === 'onlyEnabledExtensions' ? autoUpdate : true; + } + + async updateAutoUpdateValue(value: AutoUpdateConfigurationValue): Promise { + const wasEnabled = this.isAutoUpdateEnabled(); + const isEnabled = value !== false; + if (wasEnabled !== isEnabled) { + const result = await this.dialogService.confirm({ + title: nls.localize('confirmEnableDisableAutoUpdate', "Auto Update Extensions"), + message: isEnabled + ? nls.localize('confirmEnableAutoUpdate', "Do you want to enable auto update for all extensions?") + : nls.localize('confirmDisableAutoUpdate', "Do you want to disable auto update for all extensions?"), + detail: nls.localize('confirmEnableDisableAutoUpdateDetail', "This will reset any auto update settings you have set for individual extensions."), + }); + if (!result.confirmed) { + return; + } + } + await this.configurationService.updateValue(AutoUpdateConfigurationKey, value); + } + + private readonly autoRestartListenerDisposable = this._register(new MutableDisposable()); + private registerAutoRestartListener(): void { + this.autoRestartListenerDisposable.value = undefined; + if (this.configurationService.getValue(AutoRestartConfigurationKey) === true) { + this.autoRestartListenerDisposable.value = this.hostService.onDidChangeFocus(focus => { + if (!focus && this.configurationService.getValue(AutoRestartConfigurationKey) === true) { + this.updateRunningExtensions(true); + } + }); + } } private reportInstalledExtensionsTelemetry() { @@ -1200,7 +1302,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension let extension = this.getInstalledExtensionMatchingGallery(gallery); if (!extension) { extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery, undefined); - Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); + (extension).setExtensionsControlManifest(extensionsControlManifest); } return extension; } @@ -1245,7 +1347,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return undefined; } - async updateRunningExtensions(): Promise { + async updateRunningExtensions(auto: boolean = false): Promise { const toAdd: ILocalExtension[] = []; const toRemove: string[] = []; @@ -1286,8 +1388,26 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (toAdd.length || toRemove.length) { - if (await this.extensionService.stopExtensionHosts(nls.localize('restart', "Enable or Disable extensions"))) { + if (await this.extensionService.stopExtensionHosts(nls.localize('restart', "Enable or Disable extensions"), auto)) { await this.extensionService.startExtensionHosts({ toAdd, toRemove }); + if (auto) { + this.notificationService.notify({ + severity: Severity.Info, + message: nls.localize('extensionsAutoRestart', "Extensions were auto restarted to enable updates."), + priority: NotificationPriority.SILENT + }); + } + type ExtensionsAutoRestartClassification = { + owner: 'sandy081'; + comment: 'Report when extensions are auto restarted'; + count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extensions auto restarted' }; + auto: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the restart was triggered automatically' }; + }; + type ExtensionsAutoRestartEvent = { + count: number; + auto: boolean; + }; + this.telemetryService.publicLog2('extensions:autorestart', { count: toAdd.length + toRemove.length, auto }); } } } @@ -1300,7 +1420,9 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (isUninstalled) { const canRemoveRunningExtension = runningExtension && this.extensionService.canRemoveExtension(runningExtension); - const isSameExtensionRunning = runningExtension && (!extension.server || extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); + const isSameExtensionRunning = runningExtension + && (!extension.server || extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))) + && (!extension.resourceExtension || this.uriIdentityService.extUri.isEqual(extension.resourceExtension.location, runningExtension.extensionLocation)); if (!canRemoveRunningExtension && isSameExtensionRunning && !runningExtension.isUnderDevelopment) { return { action: reloadAction, reason: nls.localize('postUninstallTooltip', "Please {0} to complete the uninstallation of this extension.", reloadActionLabel) }; } @@ -1527,15 +1649,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return ExtensionState.Uninstalled; } - private async onDidAutoUpdateConfigurationChange(): Promise { - await this.updateExtensionsPinnedState(); - if (this.isAutoUpdateEnabled()) { - this.checkForUpdates(); - } else { - this.setSelectedExtensionsToAutoUpdate([]); - } - } - async checkForUpdates(onlyBuiltin?: boolean): Promise { if (!this.galleryService.isEnabled()) { return; @@ -1598,6 +1711,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension operation: InstallOperation.Update, installPreReleaseVersion: extension.local?.isPreReleaseVersion, profileLocation: this.userDataProfileService.currentProfile.extensionsResource, + isApplicationScoped: extension.local?.isApplicationScoped, } }); } @@ -1620,20 +1734,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return; } await Promise.allSettled(extensions.map(extensions => extensions.syncInstalledExtensionsWithGallery(gallery, this.getProductVersion()))); - if (this.isAutoUpdateEnabled()) { + if (this.outdated.length) { this.eventuallyAutoUpdateExtensions(); } } - getAutoUpdateValue(): AutoUpdateConfigurationValue { - const autoUpdate = this.configurationService.getValue(AutoUpdateConfigurationKey); - return isBoolean(autoUpdate) || autoUpdate === 'onlyEnabledExtensions' || autoUpdate === 'onlySelectedExtensions' ? autoUpdate : true; - } - - isAutoUpdateEnabled(): boolean { - return this.getAutoUpdateValue() !== false; - } - private isAutoCheckUpdatesEnabled(): boolean { return this.configurationService.getValue(AutoCheckUpdatesConfigurationKey); } @@ -1641,7 +1746,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private eventuallyCheckForUpdates(immediate = false): void { this.updatesCheckDelayer.cancel(); this.updatesCheckDelayer.trigger(async () => { - if (this.isAutoUpdateEnabled() || this.isAutoCheckUpdatesEnabled()) { + if (this.isAutoCheckUpdatesEnabled()) { await this.checkForUpdates(); } this.eventuallyCheckForUpdates(); @@ -1682,10 +1787,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private async autoUpdateExtensions(): Promise { - if (!this.isAutoUpdateEnabled()) { - return; - } - const toUpdate = this.outdated.filter(e => !e.local?.pinned && this.shouldAutoUpdateExtension(e)); if (!toUpdate.length) { return; @@ -1718,32 +1819,43 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return undefined; } - private async updateExtensionsPinnedState(): Promise { - await Promise.all(this.installed.map(async e => { - if (e.isBuiltin) { - return; - } - const shouldBePinned = !this.shouldAutoUpdateExtension(e); - if (e.local && e.local.pinned !== shouldBePinned) { - await this.extensionManagementService.updateMetadata(e.local, { pinned: shouldBePinned }); - } - })); - } - private shouldAutoUpdateExtension(extension: IExtension): boolean { - const autoUpdate = this.getAutoUpdateValue(); - if (isBoolean(autoUpdate)) { - return autoUpdate; + if (extension.deprecationInfo?.disallowInstall) { + return false; } - if (autoUpdate === 'onlyEnabledExtensions') { + const autoUpdateValue = this.getAutoUpdateValue(); + + if (autoUpdateValue === false) { + const extensionsToAutoUpdate = this.getEnabledAutoUpdateExtensions(); + const extensionId = extension.identifier.id.toLowerCase(); + if (extensionsToAutoUpdate.includes(extensionId)) { + return true; + } + if (this.isAutoUpdateEnabledForPublisher(extension.publisher) && !extensionsToAutoUpdate.includes(`-${extensionId}`)) { + return true; + } + return false; + } + + if (extension.pinned) { + return false; + } + + const disabledAutoUpdateExtensions = this.getDisabledAutoUpdateExtensions(); + if (disabledAutoUpdateExtensions.includes(extension.identifier.id.toLowerCase())) { + return false; + } + + if (autoUpdateValue === true) { + return true; + } + + if (autoUpdateValue === 'onlyEnabledExtensions') { return this.extensionEnablementService.isEnabledEnablementState(extension.enablementState); } - const extensionsToAutoUpdate = this.getSelectedExtensionsToAutoUpdate(); - const extensionId = extension.identifier.id.toLowerCase(); - return extensionsToAutoUpdate.includes(extensionId) || - (!extensionsToAutoUpdate.includes(`-${extensionId}`) && this.isAutoUpdateEnabledForPublisher(extension.publisher)); + return false; } isAutoUpdateEnabledFor(extensionOrPublisher: IExtension | string): boolean { @@ -1751,16 +1863,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (EXTENSION_IDENTIFIER_REGEX.test(extensionOrPublisher)) { throw new Error('Expected publisher string, found extension identifier'); } - const autoUpdate = this.getAutoUpdateValue(); - if (isBoolean(autoUpdate)) { - return autoUpdate; - } - if (autoUpdate === 'onlyEnabledExtensions') { - return false; + if (this.isAutoUpdateEnabled()) { + return true; } return this.isAutoUpdateEnabledForPublisher(extensionOrPublisher); } - return !extensionOrPublisher.local?.pinned && this.shouldAutoUpdateExtension(extensionOrPublisher); + return this.shouldAutoUpdateExtension(extensionOrPublisher); } private isAutoUpdateEnabledForPublisher(publisher: string): boolean { @@ -1769,98 +1877,135 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } async updateAutoUpdateEnablementFor(extensionOrPublisher: IExtension | string, enable: boolean): Promise { - const autoUpdateValue = this.getAutoUpdateValue(); - - if (autoUpdateValue === true || autoUpdateValue === 'onlyEnabledExtensions') { + if (this.isAutoUpdateEnabled()) { if (isString(extensionOrPublisher)) { throw new Error('Expected extension, found publisher string'); } if (!extensionOrPublisher.local) { throw new Error('Only installed extensions can be pinned'); } - await this.extensionManagementService.updateMetadata(extensionOrPublisher.local, { pinned: !enable }); - if (enable) { - this.eventuallyAutoUpdateExtensions(); - } - return; - } - if (autoUpdateValue === false && enable) { - await this.configurationService.updateValue(AutoUpdateConfigurationKey, 'onlySelectedExtensions'); - } - - let update = false; - const autoUpdateExtensions = this.getSelectedExtensionsToAutoUpdate(); - if (isString(extensionOrPublisher)) { - if (EXTENSION_IDENTIFIER_REGEX.test(extensionOrPublisher)) { - throw new Error('Expected publisher string, found extension identifier'); - } - extensionOrPublisher = extensionOrPublisher.toLowerCase(); - if (this.isAutoUpdateEnabledFor(extensionOrPublisher) !== enable) { - update = true; - if (enable) { - autoUpdateExtensions.push(extensionOrPublisher); - } else { - if (autoUpdateExtensions.includes(extensionOrPublisher)) { - autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(extensionOrPublisher), 1); - } - } - } - } else { + const disabledAutoUpdateExtensions = this.getDisabledAutoUpdateExtensions(); const extensionId = extensionOrPublisher.identifier.id.toLowerCase(); - const enableAutoUpdatesForPublisher = this.isAutoUpdateEnabledFor(extensionOrPublisher.publisher.toLowerCase()); - const enableAutoUpdatesForExtension = autoUpdateExtensions.includes(extensionId); - const disableAutoUpdatesForExtension = autoUpdateExtensions.includes(`-${extensionId}`); - + const extensionIndex = disabledAutoUpdateExtensions.indexOf(extensionId); if (enable) { - if (disableAutoUpdatesForExtension) { - autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(`-${extensionId}`), 1); - update = true; - } - if (enableAutoUpdatesForPublisher) { - if (enableAutoUpdatesForExtension) { - autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(extensionId), 1); - update = true; - } - } else { - if (!enableAutoUpdatesForExtension) { - autoUpdateExtensions.push(extensionId); - update = true; - } + if (extensionIndex !== -1) { + disabledAutoUpdateExtensions.splice(extensionIndex, 1); } } - // Disable Auto Updates else { - if (enableAutoUpdatesForExtension) { - autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(extensionId), 1); - update = true; + if (extensionIndex === -1) { + disabledAutoUpdateExtensions.push(extensionId); } - if (enableAutoUpdatesForPublisher) { - if (!disableAutoUpdatesForExtension) { - autoUpdateExtensions.push(`-${extensionId}`); - update = true; + } + this.setDisabledAutoUpdateExtensions(disabledAutoUpdateExtensions); + if (enable && extensionOrPublisher.pinned) { + await this.extensionManagementService.updateMetadata(extensionOrPublisher.local, { pinned: false }); + } + this._onChange.fire(extensionOrPublisher); + } + + else { + const enabledAutoUpdateExtensions = this.getEnabledAutoUpdateExtensions(); + if (isString(extensionOrPublisher)) { + if (EXTENSION_IDENTIFIER_REGEX.test(extensionOrPublisher)) { + throw new Error('Expected publisher string, found extension identifier'); + } + extensionOrPublisher = extensionOrPublisher.toLowerCase(); + if (this.isAutoUpdateEnabledFor(extensionOrPublisher) !== enable) { + if (enable) { + enabledAutoUpdateExtensions.push(extensionOrPublisher); + } else { + if (enabledAutoUpdateExtensions.includes(extensionOrPublisher)) { + enabledAutoUpdateExtensions.splice(enabledAutoUpdateExtensions.indexOf(extensionOrPublisher), 1); + } } - } else { + } + this.setEnabledAutoUpdateExtensions(enabledAutoUpdateExtensions); + for (const e of this.installed) { + if (e.publisher.toLowerCase() === extensionOrPublisher) { + this._onChange.fire(e); + } + } + } else { + const extensionId = extensionOrPublisher.identifier.id.toLowerCase(); + const enableAutoUpdatesForPublisher = this.isAutoUpdateEnabledFor(extensionOrPublisher.publisher.toLowerCase()); + const enableAutoUpdatesForExtension = enabledAutoUpdateExtensions.includes(extensionId); + const disableAutoUpdatesForExtension = enabledAutoUpdateExtensions.includes(`-${extensionId}`); + + if (enable) { if (disableAutoUpdatesForExtension) { - autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(`-${extensionId}`), 1); - update = true; + enabledAutoUpdateExtensions.splice(enabledAutoUpdateExtensions.indexOf(`-${extensionId}`), 1); + } + if (enableAutoUpdatesForPublisher) { + if (enableAutoUpdatesForExtension) { + enabledAutoUpdateExtensions.splice(enabledAutoUpdateExtensions.indexOf(extensionId), 1); + } + } else { + if (!enableAutoUpdatesForExtension) { + enabledAutoUpdateExtensions.push(extensionId); + } } } + // Disable Auto Updates + else { + if (enableAutoUpdatesForExtension) { + enabledAutoUpdateExtensions.splice(enabledAutoUpdateExtensions.indexOf(extensionId), 1); + } + if (enableAutoUpdatesForPublisher) { + if (!disableAutoUpdatesForExtension) { + enabledAutoUpdateExtensions.push(`-${extensionId}`); + } + } else { + if (disableAutoUpdatesForExtension) { + enabledAutoUpdateExtensions.splice(enabledAutoUpdateExtensions.indexOf(`-${extensionId}`), 1); + } + } + } + this.setEnabledAutoUpdateExtensions(enabledAutoUpdateExtensions); + this._onChange.fire(extensionOrPublisher); } } - if (update) { - this.setSelectedExtensionsToAutoUpdate(autoUpdateExtensions); - await this.onDidSelectedExtensionToAutoUpdateValueChange(true); - if (autoUpdateValue === 'onlySelectedExtensions' && autoUpdateExtensions.length === 0) { - await this.configurationService.updateValue(AutoUpdateConfigurationKey, false); - } + + if (enable) { + this.autoUpdateExtensions(); } } - private async onDidSelectedExtensionToAutoUpdateValueChange(forceUpdate: boolean): Promise { - if (forceUpdate || this.selectedExtensionsToAutoUpdateValue !== this.getSelectedExtensionsToAutoUpdateValue() /* This checks if current window changed the value or not */) { - await this.updateExtensionsPinnedState(); - this.eventuallyAutoUpdateExtensions(); + private onDidSelectedExtensionToAutoUpdateValueChange(): void { + if ( + this.enabledAuotUpdateExtensionsValue !== this.getEnabledAutoUpdateExtensionsValue() /* This checks if current window changed the value or not */ + || this.disabledAutoUpdateExtensionsValue !== this.getDisabledAutoUpdateExtensionsValue() /* This checks if current window changed the value or not */ + ) { + const userExtensions = this.installed.filter(e => !e.isBuiltin); + const groupBy = (extensions: IExtension[]): IExtension[][] => { + const shouldAutoUpdate: IExtension[] = []; + const shouldNotAutoUpdate: IExtension[] = []; + for (const extension of extensions) { + if (this.shouldAutoUpdateExtension(extension)) { + shouldAutoUpdate.push(extension); + } else { + shouldNotAutoUpdate.push(extension); + } + } + return [shouldAutoUpdate, shouldNotAutoUpdate]; + }; + + const [wasShouldAutoUpdate, wasShouldNotAutoUpdate] = groupBy(userExtensions); + this._enabledAutoUpdateExtensionsValue = undefined; + this._disabledAutoUpdateExtensionsValue = undefined; + const [shouldAutoUpdate, shouldNotAutoUpdate] = groupBy(userExtensions); + + for (const e of wasShouldAutoUpdate ?? []) { + if (shouldNotAutoUpdate?.includes(e)) { + this._onChange.fire(e); + } + } + for (const e of wasShouldNotAutoUpdate ?? []) { + if (shouldAutoUpdate?.includes(e)) { + this._onChange.fire(e); + } + } } } @@ -1929,7 +2074,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (!extension && gallery) { extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery, undefined); - Extensions.updateExtensionFromControlManifest(extension as Extension, await this.extensionManagementService.getExtensionsControlManifest()); + (extension).setExtensionsControlManifest(await this.extensionManagementService.getExtensionsControlManifest()); } if (extension?.isMalicious) { throw new Error(nls.localize('malicious', "This extension is reported to be problematic.")); @@ -2001,10 +2146,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension throw new Error(nls.localize('unknown', "Unable to install extension")); } - if (installOptions.version) { - await this.updateAutoUpdateEnablementFor(extension, false); - } - if (installOptions.enable) { if (extension.enablementState === EnablementState.DisabledWorkspace || extension.enablementState === EnablementState.DisabledGlobally) { if (installOptions.justification) { @@ -2147,7 +2288,27 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (!extension.local || isApplicationScopedExtension(extension.local.manifest) || extension.isBuiltin) { return; } - await this.extensionManagementService.toggleAppliationScope(extension.local, this.userDataProfileService.currentProfile.extensionsResource); + const isApplicationScoped = extension.local.isApplicationScoped; + await Promise.all(this.getAllExtensions().map(async extensions => { + const local = extensions.local.find(e => areSameExtensions(e.identifier, extension.identifier))?.local; + if (local && local.isApplicationScoped === isApplicationScoped) { + await this.extensionManagementService.toggleAppliationScope(local, this.userDataProfileService.currentProfile.extensionsResource); + } + })); + } + + private getAllExtensions(): Extensions[] { + const extensions: Extensions[] = []; + if (this.localExtensions) { + extensions.push(this.localExtensions); + } + if (this.remoteExtensions) { + extensions.push(this.remoteExtensions); + } + if (this.webExtensions) { + extensions.push(this.webExtensions); + } + return extensions; } private isInstalledExtensionSynced(extension: ILocalExtension): boolean { @@ -2468,12 +2629,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private getPublishersToAutoUpdate(): string[] { - return this.getSelectedExtensionsToAutoUpdate().filter(id => !EXTENSION_IDENTIFIER_REGEX.test(id)); + return this.getEnabledAutoUpdateExtensions().filter(id => !EXTENSION_IDENTIFIER_REGEX.test(id)); } - getSelectedExtensionsToAutoUpdate(): string[] { + getEnabledAutoUpdateExtensions(): string[] { try { - const parsedValue = JSON.parse(this.selectedExtensionsToAutoUpdateValue); + const parsedValue = JSON.parse(this.enabledAuotUpdateExtensionsValue); if (Array.isArray(parsedValue)) { return parsedValue; } @@ -2481,32 +2642,70 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return []; } - private setSelectedExtensionsToAutoUpdate(selectedExtensionsToAutoUpdate: string[]): void { - this.selectedExtensionsToAutoUpdateValue = JSON.stringify(selectedExtensionsToAutoUpdate); + private setEnabledAutoUpdateExtensions(enabledAutoUpdateExtensions: string[]): void { + this.enabledAuotUpdateExtensionsValue = JSON.stringify(enabledAutoUpdateExtensions); } - private _selectedExtensionsToAutoUpdateValue: string | undefined; - private get selectedExtensionsToAutoUpdateValue(): string { - if (!this._selectedExtensionsToAutoUpdateValue) { - this._selectedExtensionsToAutoUpdateValue = this.getSelectedExtensionsToAutoUpdateValue(); + private _enabledAutoUpdateExtensionsValue: string | undefined; + private get enabledAuotUpdateExtensionsValue(): string { + if (!this._enabledAutoUpdateExtensionsValue) { + this._enabledAutoUpdateExtensionsValue = this.getEnabledAutoUpdateExtensionsValue(); } - return this._selectedExtensionsToAutoUpdateValue; + return this._enabledAutoUpdateExtensionsValue; } - private set selectedExtensionsToAutoUpdateValue(placeholderViewContainesValue: string) { - if (this.selectedExtensionsToAutoUpdateValue !== placeholderViewContainesValue) { - this._selectedExtensionsToAutoUpdateValue = placeholderViewContainesValue; - this.setSelectedExtensionsToAutoUpdateValue(placeholderViewContainesValue); + private set enabledAuotUpdateExtensionsValue(enabledAuotUpdateExtensionsValue: string) { + if (this.enabledAuotUpdateExtensionsValue !== enabledAuotUpdateExtensionsValue) { + this._enabledAutoUpdateExtensionsValue = enabledAuotUpdateExtensionsValue; + this.setEnabledAutoUpdateExtensionsValue(enabledAuotUpdateExtensionsValue); } } - private getSelectedExtensionsToAutoUpdateValue(): string { + private getEnabledAutoUpdateExtensionsValue(): string { return this.storageService.get(EXTENSIONS_AUTO_UPDATE_KEY, StorageScope.APPLICATION, '[]'); } - private setSelectedExtensionsToAutoUpdateValue(value: string): void { + private setEnabledAutoUpdateExtensionsValue(value: string): void { this.storageService.store(EXTENSIONS_AUTO_UPDATE_KEY, value, StorageScope.APPLICATION, StorageTarget.USER); } + getDisabledAutoUpdateExtensions(): string[] { + try { + const parsedValue = JSON.parse(this.disabledAutoUpdateExtensionsValue); + if (Array.isArray(parsedValue)) { + return parsedValue; + } + } catch (e) { /* Ignore */ } + return []; + } + + private setDisabledAutoUpdateExtensions(disabledAutoUpdateExtensions: string[]): void { + this.disabledAutoUpdateExtensionsValue = JSON.stringify(disabledAutoUpdateExtensions); + } + + private _disabledAutoUpdateExtensionsValue: string | undefined; + private get disabledAutoUpdateExtensionsValue(): string { + if (!this._disabledAutoUpdateExtensionsValue) { + this._disabledAutoUpdateExtensionsValue = this.getDisabledAutoUpdateExtensionsValue(); + } + + return this._disabledAutoUpdateExtensionsValue; + } + + private set disabledAutoUpdateExtensionsValue(disabledAutoUpdateExtensionsValue: string) { + if (this.disabledAutoUpdateExtensionsValue !== disabledAutoUpdateExtensionsValue) { + this._disabledAutoUpdateExtensionsValue = disabledAutoUpdateExtensionsValue; + this.setDisabledAutoUpdateExtensionsValue(disabledAutoUpdateExtensionsValue); + } + } + + private getDisabledAutoUpdateExtensionsValue(): string { + return this.storageService.get(EXTENSIONS_DONOT_AUTO_UPDATE_KEY, StorageScope.APPLICATION, '[]'); + } + + private setDisabledAutoUpdateExtensionsValue(value: string): void { + this.storageService.store(EXTENSIONS_DONOT_AUTO_UPDATE_KEY, value, StorageScope.APPLICATION, StorageTarget.USER); + } + } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css index 7bca4243703..ed8c3395ccc 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css @@ -74,15 +74,15 @@ border-bottom: 1px solid var(--vscode-contrastBorder); } -.monaco-action-bar .action-item .action-label.extension-action.extension-status-error { +.monaco-action-bar .action-item .action-label.extension-action.extension-status-error::before { color: var(--vscode-editorError-foreground); } -.monaco-action-bar .action-item .action-label.extension-action.extension-status-warning { +.monaco-action-bar .action-item .action-label.extension-action.extension-status-warning::before { color: var(--vscode-editorWarning-foreground); } -.monaco-action-bar .action-item .action-label.extension-action.extension-status-info { +.monaco-action-bar .action-item .action-label.extension-action.extension-status-info::before { color: var(--vscode-editorInfo-foreground); } @@ -97,7 +97,8 @@ .monaco-action-bar .action-item.disabled .action-label.extension-action.hide, .monaco-action-bar .action-item.disabled .action-label.extension-action.ignore, .monaco-action-bar .action-item.disabled .action-label.extension-action.undo-ignore, -.monaco-action-bar .action-item.disabled .action-label.extension-action.install:not(.installing), +.monaco-action-bar .action-item .action-label.extension-action.install.hide, +.monaco-action-bar .action-item.disabled .action-label.extension-action.install-other-server:not(.installing), .monaco-action-bar .action-item.disabled .action-label.extension-action.uninstall:not(.uninstalling), .monaco-action-bar .action-item.disabled .action-label.extension-action.hide-when-disabled, .monaco-action-bar .action-item.disabled .action-label.extension-action.update, @@ -105,7 +106,7 @@ .monaco-action-bar .action-item.disabled .action-label.extension-action.theme, .monaco-action-bar .action-item.disabled .action-label.extension-action.language, .monaco-action-bar .action-item.disabled .action-label.extension-action.extension-sync, -.monaco-action-bar .action-item.action-dropdown-item.disabled, +.monaco-action-bar .action-item.action-dropdown-item.hide, .monaco-action-bar .action-item.action-dropdown-item .action-label.extension-action.hide, .monaco-action-bar .action-item.disabled .action-label.extension-action.reload, .monaco-action-bar .action-item.disabled .action-label.disable-status.hide, @@ -115,6 +116,10 @@ display: none; } +.monaco-action-bar .action-item.disabled .action-label.extension-action.label { + opacity: 0.4 !important; +} + .monaco-action-bar .action-item.checkbox-action-item.disabled { display: none; } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 59a07a33c6f..1de72e81ee9 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -215,7 +215,6 @@ } .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item:not(.checkbox-action-item) .extension-action:not(.icon) { - border-radius: 0; padding-top: 0; padding-bottom: 0; } @@ -486,6 +485,7 @@ overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + text-decoration: var(--text-link-decoration); } .extension-editor > .body > .content > .details > .additional-details-container .resources-container > .resources > .resource:hover { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css index d77881dfa39..9179fa761d3 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css @@ -39,6 +39,7 @@ .extension-verified-publisher > .extension-verified-publisher-domain { padding-left: 2px; color: var(--vscode-extensionIcon-verifiedForeground); + text-decoration: var(--text-link-decoration); } .extension-bookmark { diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 737e37568bc..c619e3a0b57 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -62,6 +62,7 @@ export interface IExtension { readonly publisherUrl?: URI; readonly publisherDomain?: { link: string; verified: boolean }; readonly publisherSponsorLink?: URI; + readonly pinned: boolean; readonly version: string; readonly latestVersion: string; readonly preRelease: boolean; @@ -138,7 +139,7 @@ export interface IExtensionsWorkbenchService { isAutoUpdateEnabledFor(extensionOrPublisher: IExtension | string): boolean; updateAutoUpdateEnablementFor(extensionOrPublisher: IExtension | string, enable: boolean): Promise; open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise; - isAutoUpdateEnabled(): boolean; + updateAutoUpdateValue(value: AutoUpdateConfigurationValue): Promise; getAutoUpdateValue(): AutoUpdateConfigurationValue; checkForUpdates(): Promise; getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined; @@ -163,6 +164,7 @@ export const ConfigurationKey = 'extensions'; export const AutoUpdateConfigurationKey = 'extensions.autoUpdate'; export const AutoCheckUpdatesConfigurationKey = 'extensions.autoCheckUpdates'; export const CloseExtensionDetailsOnViewChangeKey = 'extensions.closeExtensionDetailsOnViewChange'; +export const AutoRestartConfigurationKey = 'extensions.autoRestart'; export type AutoUpdateConfigurationValue = boolean | 'onlyEnabledExtensions' | 'onlySelectedExtensions'; @@ -230,3 +232,9 @@ export const INSTALL_ACTIONS_GROUP = '0_install'; export const UPDATE_ACTIONS_GROUP = '0_update'; export const extensionsSearchActionsMenu = new MenuId('extensionsSearchActionsMenu'); + +export interface IExtensionArg { + id: string; + version: string; + location: URI | undefined; +} diff --git a/src/vs/workbench/contrib/extensions/common/reportExtensionIssueAction.ts b/src/vs/workbench/contrib/extensions/common/reportExtensionIssueAction.ts index 75304460374..34a1b543b5b 100644 --- a/src/vs/workbench/contrib/extensions/common/reportExtensionIssueAction.ts +++ b/src/vs/workbench/contrib/extensions/common/reportExtensionIssueAction.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; export class ReportExtensionIssueAction extends Action { diff --git a/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts b/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts index a743e9e184e..588f6a3cec5 100644 --- a/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts +++ b/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; suite('Extension query', () => { diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts index 8c96cf7a48e..efdcd3abd98 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extension.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions'; import { Extension } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { IGalleryExtension, IGalleryExtensionProperties, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index 4e62c45838c..be1ca52ad70 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as sinon from 'sinon'; -import * as assert from 'assert'; +import assert from 'assert'; import * as uuid from 'vs/base/common/uuid'; import { IExtensionGalleryService, IGalleryExtensionAssets, IGalleryExtension, IExtensionManagementService, IExtensionTipsService, getTargetPlatform, diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index 00c2b9a6eec..197f6059031 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { generateUuid } from 'vs/base/common/uuid'; import { IExtensionsWorkbenchService, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions'; import * as ExtensionsActions from 'vs/workbench/contrib/extensions/browser/extensionsActions'; @@ -60,6 +60,9 @@ import { IUpdateService, State } from 'vs/platform/update/common/update'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { Mutable } from 'vs/base/common/types'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -126,6 +129,7 @@ function setupTest(disposables: Pick) { } }); + instantiationService.stub(IUserDataProfileService, disposables.add(new UserDataProfileService(toUserDataProfile('test', 'test', URI.file('foo'), URI.file('cache'))))); instantiationService.stub(IWorkbenchExtensionEnablementService, disposables.add(new TestExtensionEnablementService(instantiationService))); instantiationService.stub(ILabelService, { onDidChangeFormatters: disposables.add(new Emitter()).event }); @@ -173,7 +177,7 @@ suite('ExtensionsActions', () => { testObject.extension = paged.firstPage[0]; assert.ok(!testObject.enabled); assert.strictEqual('Install', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install hide', testObject.class); }); }); }); @@ -187,7 +191,7 @@ suite('ExtensionsActions', () => { return workbenchService.queryGallery(CancellationToken.None) .then((paged) => { testObject.extension = paged.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(!testObject.enabled); assert.strictEqual('Installing', testObject.label); @@ -202,7 +206,7 @@ suite('ExtensionsActions', () => { const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); const paged = await workbenchService.queryGallery(CancellationToken.None); - const promise = Event.toPromise(testObject.onDidChange); + const promise = Event.toPromise(Event.filter(testObject.onDidChange, e => e.enabled === true)); testObject.extension = paged.firstPage[0]; await promise; assert.ok(testObject.enabled); @@ -217,8 +221,8 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -232,8 +236,8 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -255,7 +259,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); assert.strictEqual('Uninstalling', testObject.label); assert.strictEqual('extension-action label uninstall uninstalling', testObject.class); @@ -303,7 +307,7 @@ suite('ExtensionsActions', () => { const gallery = aGalleryExtension('a'); const extension = extensions[0]; extension.gallery = gallery; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); testObject.extension = extension; assert.ok(!testObject.enabled); }); @@ -318,9 +322,9 @@ suite('ExtensionsActions', () => { const paged = await instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None); testObject.extension = paged.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); const promise = Event.toPromise(testObject.onDidChange); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); await promise; assert.ok(testObject.enabled); @@ -429,7 +433,7 @@ suite('ExtensionsActions', () => { c(); } })); - installEvent.fire({ identifier: local.identifier, source: gallery }); + installEvent.fire({ identifier: local.identifier, source: gallery, profileLocation: null! }); }); }); @@ -480,7 +484,7 @@ suite('ExtensionsActions', () => { .then(page => { testObject.extension = page.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(!testObject.enabled); assert.strictEqual('extension-action icon manage codicon codicon-extensions-manage hide', testObject.class); assert.strictEqual('Manage', testObject.tooltip); @@ -495,9 +499,9 @@ suite('ExtensionsActions', () => { const paged = await instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None); testObject.extension = paged.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); const promise = Event.toPromise(testObject.onDidChange); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); await promise; assert.ok(testObject.enabled); @@ -529,7 +533,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); assert.strictEqual('extension-action icon manage codicon codicon-extensions-manage', testObject.class); @@ -735,7 +739,7 @@ suite('ExtensionsActions', () => { testObject.extension = page.firstPage[0]; disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(!testObject.enabled); }); }); @@ -748,7 +752,7 @@ suite('ExtensionsActions', () => { .then(extensions => { const testObject: ExtensionsActions.EnableDropDownAction = disposables.add(instantiationService.createInstance(ExtensionsActions.EnableDropDownAction)); testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); }); @@ -929,7 +933,7 @@ suite('ExtensionsActions', () => { const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = page.firstPage[0]; disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(!testObject.enabled); }); }); @@ -948,7 +952,7 @@ suite('ExtensionsActions', () => { const testObject: ExtensionsActions.DisableGloballyAction = disposables.add(instantiationService.createInstance(ExtensionsActions.DisableGloballyAction)); testObject.extension = extensions[0]; disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); }); @@ -976,7 +980,7 @@ suite('ExtensionRuntimeStateAction', () => { instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); const paged = await workbenchService.queryGallery(CancellationToken.None); testObject.extension = paged.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -989,7 +993,7 @@ suite('ExtensionRuntimeStateAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -1010,9 +1014,9 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = paged.firstPage[0]; assert.ok(!testObject.enabled); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); const promise = Event.toPromise(testObject.onDidChange); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); await promise; assert.ok(testObject.enabled); assert.strictEqual(testObject.tooltip, `Please restart extensions to enable this extension.`); @@ -1035,8 +1039,8 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = paged.firstPage[0]; assert.ok(!testObject.enabled); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); assert.ok(!testObject.enabled); }); @@ -1056,10 +1060,10 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = paged.firstPage[0]; const identifier = gallery.identifier; - installEvent.fire({ identifier, source: gallery }); - didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, { identifier }) }]); - uninstallEvent.fire({ identifier }); - didUninstallEvent.fire({ identifier }); + installEvent.fire({ identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, { identifier }), profileLocation: null! }]); + uninstallEvent.fire({ identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -1080,8 +1084,8 @@ suite('ExtensionRuntimeStateAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(testObject.enabled); assert.strictEqual(testObject.tooltip, `Please restart extensions to complete the uninstallation of this extension.`); }); @@ -1101,8 +1105,8 @@ suite('ExtensionRuntimeStateAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -1121,13 +1125,13 @@ suite('ExtensionRuntimeStateAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); const gallery = aGalleryExtension('a'); const identifier = gallery.identifier; - installEvent.fire({ identifier, source: gallery }); - didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local }]); + installEvent.fire({ identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local, profileLocation: null! }]); assert.ok(!testObject.enabled); }); @@ -1156,8 +1160,8 @@ suite('ExtensionRuntimeStateAction', () => { } })); const gallery = aGalleryExtension('a', { uuid: local.identifier.id, version: '1.0.2' }); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); }); }); @@ -1179,8 +1183,8 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = extensions[0]; const gallery = aGalleryExtension('a', { identifier: local.identifier, version: '1.0.2' }); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Update, local: aLocalExtension('a', gallery, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Update, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); assert.ok(!testObject.enabled); }); @@ -1290,8 +1294,8 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = extensions[0]; const gallery = aGalleryExtension('a', { identifier: local.identifier, version: '1.0.2' }); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery), profileLocation: null! }]); await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); @@ -1315,8 +1319,8 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = paged.firstPage[0]; assert.ok(!testObject.enabled); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', { ...gallery, ...{ contributes: { localizations: [{ languageId: 'de', translations: [] }] } } }, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', { ...gallery, ...{ contributes: { localizations: [{ languageId: 'de', translations: [] }] } } }, gallery), profileLocation: null! }]); assert.ok(!testObject.enabled); }); @@ -1337,8 +1341,8 @@ suite('ExtensionRuntimeStateAction', () => { testObject.extension = extensions[0]; const gallery = aGalleryExtension('a', { uuid: local.identifier.id, version: '1.0.2' }); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', { ...gallery, ...{ contributes: { localizations: [{ languageId: 'de', translations: [] }] } } }, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', { ...gallery, ...{ contributes: { localizations: [{ languageId: 'de', translations: [] }] } } }, gallery), profileLocation: null! }]); assert.ok(!testObject.enabled); }); @@ -1378,7 +1382,7 @@ suite('ExtensionRuntimeStateAction', () => { const remoteExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); const localExtensionManagementService = createExtensionManagementService([localExtension]); const uninstallEvent = new Emitter(); - const onDidUninstallEvent = new Emitter<{ identifier: IExtensionIdentifier }>(); + const onDidUninstallEvent = new Emitter<{ identifier: IExtensionIdentifier; profileLocation: URI }>(); localExtensionManagementService.onUninstallExtension = uninstallEvent.event; localExtensionManagementService.onDidUninstallExtension = onDidUninstallEvent.event; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, localExtensionManagementService, createExtensionManagementService([remoteExtension])); @@ -1404,8 +1408,8 @@ suite('ExtensionRuntimeStateAction', () => { assert.ok(testObject.extension); assert.ok(!testObject.enabled); - uninstallEvent.fire({ identifier: localExtension.identifier }); - didUninstallEvent.fire({ identifier: localExtension.identifier }); + uninstallEvent.fire({ identifier: localExtension.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: localExtension.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -1442,7 +1446,7 @@ suite('ExtensionRuntimeStateAction', () => { const remoteExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); const promise = Event.toPromise(testObject.onDidChange); - onDidInstallEvent.fire([{ identifier: remoteExtension.identifier, local: remoteExtension, operation: InstallOperation.Install }]); + onDidInstallEvent.fire([{ identifier: remoteExtension.identifier, local: remoteExtension, operation: InstallOperation.Install, profileLocation: null! }]); await promise; assert.ok(testObject.enabled); @@ -1481,7 +1485,7 @@ suite('ExtensionRuntimeStateAction', () => { const localExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file('pub.a') }); const promise = Event.toPromise(Event.filter(testObject.onDidChange, () => testObject.enabled)); - onDidInstallEvent.fire([{ identifier: localExtension.identifier, local: localExtension, operation: InstallOperation.Install }]); + onDidInstallEvent.fire([{ identifier: localExtension.identifier, local: localExtension, operation: InstallOperation.Install, profileLocation: null! }]); await promise; assert.ok(testObject.enabled); @@ -1729,7 +1733,7 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test remote install action when installing local workspace extension', async () => { @@ -1755,12 +1759,12 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); - onInstallExtension.fire({ identifier: localWorkspaceExtension.identifier, source: gallery }); + onInstallExtension.fire({ identifier: localWorkspaceExtension.identifier, source: gallery, profileLocation: null! }); assert.ok(testObject.enabled); assert.strictEqual('Installing', testObject.label); - assert.strictEqual('extension-action label install installing', testObject.class); + assert.strictEqual('extension-action label install-other-server installing', testObject.class); }); test('Test remote install action when installing local workspace extension is finished', async () => { @@ -1788,16 +1792,16 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); - onInstallExtension.fire({ identifier: localWorkspaceExtension.identifier, source: gallery }); + onInstallExtension.fire({ identifier: localWorkspaceExtension.identifier, source: gallery, profileLocation: null! }); assert.ok(testObject.enabled); assert.strictEqual('Installing', testObject.label); - assert.strictEqual('extension-action label install installing', testObject.class); + assert.strictEqual('extension-action label install-other-server installing', testObject.class); const installedExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); const promise = Event.toPromise(testObject.onDidChange); - onDidInstallEvent.fire([{ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install }]); + onDidInstallEvent.fire([{ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install, profileLocation: null! }]); await promise; assert.ok(!testObject.enabled); }); @@ -1822,7 +1826,7 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test remote install action is enabled local workspace+ui extension', async () => { @@ -1844,7 +1848,7 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test remote install action is enabled for local ui+workapace extension if can install is true', async () => { @@ -1866,7 +1870,7 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test remote install action is disabled for local ui+workapace extension if can install is false', async () => { @@ -1987,7 +1991,7 @@ suite('RemoteInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - uninstallEvent.fire({ identifier: localWorkspaceExtension.identifier }); + uninstallEvent.fire({ identifier: localWorkspaceExtension.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -2107,7 +2111,7 @@ suite('RemoteInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test remote install action is disabled if local language pack extension is uninstalled', async () => { @@ -2131,7 +2135,7 @@ suite('RemoteInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - uninstallEvent.fire({ identifier: languagePackExtension.identifier }); + uninstallEvent.fire({ identifier: languagePackExtension.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); }); @@ -2160,7 +2164,7 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test local install action is enabled for remote ui+workspace extension', async () => { @@ -2181,7 +2185,7 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test local install action when installing remote ui extension', async () => { @@ -2207,12 +2211,12 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); - onInstallExtension.fire({ identifier: remoteUIExtension.identifier, source: gallery }); + onInstallExtension.fire({ identifier: remoteUIExtension.identifier, source: gallery, profileLocation: null! }); assert.ok(testObject.enabled); assert.strictEqual('Installing', testObject.label); - assert.strictEqual('extension-action label install installing', testObject.class); + assert.strictEqual('extension-action label install-other-server installing', testObject.class); }); test('Test local install action when installing remote ui extension is finished', async () => { @@ -2240,16 +2244,16 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); - onInstallExtension.fire({ identifier: remoteUIExtension.identifier, source: gallery }); + onInstallExtension.fire({ identifier: remoteUIExtension.identifier, source: gallery, profileLocation: null! }); assert.ok(testObject.enabled); assert.strictEqual('Installing', testObject.label); - assert.strictEqual('extension-action label install installing', testObject.class); + assert.strictEqual('extension-action label install-other-server installing', testObject.class); const installedExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); const promise = Event.toPromise(testObject.onDidChange); - onDidInstallEvent.fire([{ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install }]); + onDidInstallEvent.fire([{ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install, profileLocation: null! }]); await promise; assert.ok(!testObject.enabled); }); @@ -2274,7 +2278,7 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test local install action is disabled when extension is not set', async () => { @@ -2399,7 +2403,7 @@ suite('LocalInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - uninstallEvent.fire({ identifier: remoteUIExtension.identifier }); + uninstallEvent.fire({ identifier: remoteUIExtension.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -2498,7 +2502,7 @@ suite('LocalInstallAction', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - assert.strictEqual('extension-action label prominent install', testObject.class); + assert.strictEqual('extension-action label prominent install-other-server', testObject.class); }); test('Test local install action is disabled if remote language pack extension is uninstalled', async () => { @@ -2522,7 +2526,7 @@ suite('LocalInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - uninstallEvent.fire({ identifier: languagePackExtension.identifier }); + uninstallEvent.fire({ identifier: languagePackExtension.identifier, profileLocation: null! }); assert.ok(!testObject.enabled); }); @@ -2638,7 +2642,7 @@ function createExtensionManagementService(installed: ILocalExtension[] = []): IP getInstalled: () => Promise.resolve(installed), canInstall: async (extension: IGalleryExtension) => { return true; }, installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), - updateMetadata: async (local: Mutable, metadata: Partial) => { + updateMetadata: async (local: Mutable, metadata: Partial, profileLocation: URI) => { local.identifier.uuid = metadata.id; local.publisherDisplayName = metadata.publisherDisplayName!; local.publisherId = metadata.publisherId!; @@ -2648,5 +2652,3 @@ function createExtensionManagementService(installed: ILocalExtension[] = []): IP async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, }; } - - diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 7ff4ea1b27a..ef60bf96605 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { generateUuid } from 'vs/base/common/uuid'; import { ExtensionsListView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -51,6 +51,9 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { IUpdateService, State } from 'vs/platform/update/common/update'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; suite('ExtensionsViews Tests', () => { @@ -122,6 +125,7 @@ suite('ExtensionsViews Tests', () => { }); instantiationService.stub(IWorkbenchExtensionEnablementService, disposableStore.add(new TestExtensionEnablementService(instantiationService))); + instantiationService.stub(IUserDataProfileService, disposableStore.add(new UserDataProfileService(toUserDataProfile('test', 'test', URI.file('foo'), URI.file('cache'))))); const reasons: { [key: string]: any } = {}; reasons[workspaceRecommendationA.identifier.id] = { reasonId: ExtensionRecommendationReason.Workspace }; @@ -501,9 +505,6 @@ suite('ExtensionsViews Tests', () => { }] }); - testableView.dispose(); - testableView = disposableStore.add(instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); - return testableView.show('search-me').then(result => { const options: IQueryOptions = queryTarget.args[0][0]; @@ -528,9 +529,6 @@ suite('ExtensionsViews Tests', () => { const queryTarget = instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...realResults)); - testableView.dispose(); - disposableStore.add(testableView = instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); - return testableView.show('search-me @sort:installs').then(result => { const options: IQueryOptions = queryTarget.args[0][0]; @@ -570,4 +568,3 @@ suite('ExtensionsViews Tests', () => { } }); - diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index e42cf86167d..eee8b207a29 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as sinon from 'sinon'; -import * as assert from 'assert'; +import assert from 'assert'; import { generateUuid } from 'vs/base/common/uuid'; import { ExtensionState, AutoCheckUpdatesConfigurationKey, AutoUpdateConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; @@ -25,7 +25,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { TestExtensionTipsService, TestSharedProcessService } from 'vs/workbench/test/electron-sandbox/workbenchTestServices'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { ProgressService } from 'vs/workbench/services/progress/browser/progressService'; @@ -54,6 +54,10 @@ import { Mutable } from 'vs/base/common/types'; import { IUpdateService, State } from 'vs/platform/update/common/update'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -89,6 +93,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { stubConfiguration(); instantiationService.stub(IRemoteAgentService, RemoteAgentService); + instantiationService.stub(IUserDataProfileService, disposableStore.add(new UserDataProfileService(toUserDataProfile('test', 'test', URI.file('foo'), URI.file('cache'))))); instantiationService.stub(IWorkbenchExtensionManagementService, { onDidInstallExtensions: didInstallEvent.event, @@ -107,7 +112,8 @@ suite('ExtensionsWorkbenchServiceTest', () => { return local; }, async canInstall() { return true; }, - getTargetPlatform: async () => getTargetPlatform(platform, arch) + getTargetPlatform: async () => getTargetPlatform(platform, arch), + async resetPinnedStateForAllUserExtensions(pinned: boolean) { } }); instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({ @@ -374,7 +380,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const identifier = gallery.identifier; // Installing - installEvent.fire({ identifier, source: gallery }); + installEvent.fire({ identifier, source: gallery, profileLocation: null! }); const local = testObject.local; assert.strictEqual(1, local.length); const actual = local[0]; @@ -382,18 +388,18 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.strictEqual(ExtensionState.Installing, actual.state); // Installed - didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension(gallery.name, gallery, { identifier }) }]); + didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension(gallery.name, gallery, { identifier }), profileLocation: null! }]); assert.strictEqual(ExtensionState.Installed, actual.state); assert.strictEqual(1, testObject.local.length); testObject.uninstall(actual); // Uninstalling - uninstallEvent.fire({ identifier }); + uninstallEvent.fire({ identifier, profileLocation: null! }); assert.strictEqual(ExtensionState.Uninstalling, actual.state); // Uninstalled - didUninstallEvent.fire({ identifier }); + didUninstallEvent.fire({ identifier, profileLocation: null! }); assert.strictEqual(ExtensionState.Uninstalled, actual.state); assert.strictEqual(0, testObject.local.length); @@ -416,8 +422,8 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); const target = testObject.local[0]; testObject.uninstall(target); - uninstallEvent.fire({ identifier: local.identifier }); - didUninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(!(await testObject.canInstall(target))); }); @@ -455,11 +461,11 @@ suite('ExtensionsWorkbenchServiceTest', () => { const extension = page.firstPage[0]; assert.strictEqual(ExtensionState.Uninstalled, extension.state); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); const promise = Event.toPromise(testObject.onChange); // Installed - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension(gallery.name, gallery, gallery) }]); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension(gallery.name, gallery, gallery), profileLocation: null! }]); await promise; }); @@ -477,7 +483,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { disposableStore.add(testObject.onChange(target)); // Installing - installEvent.fire({ identifier: gallery.identifier, source: gallery }); + installEvent.fire({ identifier: gallery.identifier, source: gallery, profileLocation: null! }); assert.ok(target.calledOnce); }); @@ -491,7 +497,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject.uninstall(testObject.local[0]); disposableStore.add(testObject.onChange(target)); - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(target.calledOnce); }); @@ -503,9 +509,9 @@ suite('ExtensionsWorkbenchServiceTest', () => { const target = sinon.spy(); testObject.uninstall(testObject.local[0]); - uninstallEvent.fire({ identifier: local.identifier }); + uninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); disposableStore.add(testObject.onChange(target)); - didUninstallEvent.fire({ identifier: local.identifier }); + didUninstallEvent.fire({ identifier: local.identifier, profileLocation: null! }); assert.ok(target.calledOnce); }); @@ -1018,7 +1024,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); const local = aLocalExtension('pub.a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); - didInstallEvent.fire([{ local, identifier: local.identifier, operation: InstallOperation.Update }]); + didInstallEvent.fire([{ local, identifier: local.identifier, operation: InstallOperation.Update, profileLocation: null! }]); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const actual = await testObject.queryLocal(); assert.strictEqual(actual[0].enablementState, EnablementState.DisabledGlobally); @@ -1028,7 +1034,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); const local = aLocalExtension('pub.a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledWorkspace); - didInstallEvent.fire([{ local, identifier: local.identifier, operation: InstallOperation.Update }]); + didInstallEvent.fire([{ local, identifier: local.identifier, operation: InstallOperation.Update, profileLocation: null! }]); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const actual = await testObject.queryLocal(); assert.strictEqual(actual[0].enablementState, EnablementState.DisabledWorkspace); @@ -1427,10 +1433,35 @@ suite('ExtensionsWorkbenchServiceTest', () => { await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); - assert.strictEqual(testObject.local[0].local?.pinned, true); + assert.strictEqual(testObject.local[0].local?.pinned, undefined); assert.strictEqual(testObject.local[1].local?.pinned, undefined); - assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), []); + assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), ['pub.a']); + }); + + test('Test disable autoupdate for extension when auto update is enabled for enabled extensions', async () => { + stubConfiguration('onlyEnabledExtensions'); + + const extension1 = aLocalExtension('a'); + const extension2 = aLocalExtension('b'); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: Mutable, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + assert.strictEqual(testObject.local[0].local?.pinned, undefined); + assert.strictEqual(testObject.local[1].local?.pinned, undefined); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); + + assert.strictEqual(testObject.local[0].local?.pinned, undefined); + assert.strictEqual(testObject.local[1].local?.pinned, undefined); + + assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), ['pub.a']); }); test('Test enable autoupdate for extension when auto update is enabled for all', async () => { @@ -1449,10 +1480,33 @@ suite('ExtensionsWorkbenchServiceTest', () => { await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); + assert.strictEqual(testObject.local[0].local?.pinned, undefined); + assert.strictEqual(testObject.local[1].local?.pinned, undefined); + + assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); + }); + + test('Test enable autoupdate for pinned extension when auto update is enabled', async () => { + const extension1 = aLocalExtension('a', undefined, { pinned: true }); + const extension2 = aLocalExtension('b'); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: Mutable, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + assert.strictEqual(testObject.local[0].local?.pinned, true); + assert.strictEqual(testObject.local[1].local?.pinned, undefined); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); + assert.strictEqual(testObject.local[0].local?.pinned, false); assert.strictEqual(testObject.local[1].local?.pinned, undefined); - assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), []); + assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); }); test('Test updateAutoUpdateEnablementFor throws error when auto update is disabled', async () => { @@ -1485,46 +1539,6 @@ suite('ExtensionsWorkbenchServiceTest', () => { } }); - test('Test updateAutoUpdateEnablementFor throws error for extension id when auto update mode is onlySelectedExtensions', async () => { - stubConfiguration('onlySelectedExtensions'); - - const extension1 = aLocalExtension('a'); - const extension2 = aLocalExtension('b'); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); - testObject = await aWorkbenchService(); - - try { - await testObject.updateAutoUpdateEnablementFor(testObject.local[0].identifier.id, true); - assert.fail('error expected'); - } catch (error) { - // expected - } - }); - - test('Test enable autoupdate for extension when auto update is set to onlySelectedExtensions', async () => { - stubConfiguration('onlySelectedExtensions'); - - const extension1 = aLocalExtension('a', undefined, { pinned: true }); - const extension2 = aLocalExtension('b', undefined, { pinned: true }); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); - instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: Mutable, metadata: Partial) => { - local.pinned = !!metadata.pinned; - return local; - }); - testObject = await aWorkbenchService(); - - assert.strictEqual(testObject.local[0].local?.pinned, true); - assert.strictEqual(testObject.local[1].local?.pinned, true); - - await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); - - assert.strictEqual(testObject.local[0].local?.pinned, false); - assert.strictEqual(testObject.local[1].local?.pinned, true); - - assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), ['pub.a']); - assert.equal(instantiationService.get(IConfigurationService).getValue(AutoUpdateConfigurationKey), 'onlySelectedExtensions'); - }); - test('Test enable autoupdate for extension when auto update is disabled', async () => { stubConfiguration(false); @@ -1542,15 +1556,43 @@ suite('ExtensionsWorkbenchServiceTest', () => { await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); - assert.strictEqual(testObject.local[0].local?.pinned, false); + assert.strictEqual(testObject.local[0].local?.pinned, true); assert.strictEqual(testObject.local[1].local?.pinned, true); - assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), ['pub.a']); - assert.equal(instantiationService.get(IConfigurationService).getValue(AutoUpdateConfigurationKey), 'onlySelectedExtensions'); + assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), ['pub.a']); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); }); - test('Test disable autoupdate for extension when auto update is set to onlySelectedExtensions', async () => { - stubConfiguration('onlySelectedExtensions'); + test('Test reset autoupdate extensions state when auto update is disabled', async () => { + instantiationService.stub(IDialogService, { + confirm: () => Promise.resolve({ confirmed: true }) + }); + + const extension1 = aLocalExtension('a', undefined, { pinned: true }); + const extension2 = aLocalExtension('b', undefined, { pinned: true }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2]); + instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: Mutable, metadata: Partial) => { + local.pinned = !!metadata.pinned; + return local; + }); + testObject = await aWorkbenchService(); + + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); + + assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), ['pub.a']); + + await testObject.updateAutoUpdateValue(false); + + assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); + }); + + test('Test reset autoupdate extensions state when auto update is enabled', async () => { + stubConfiguration(false); + instantiationService.stub(IDialogService, { + confirm: () => Promise.resolve({ confirmed: true }) + }); const extension1 = aLocalExtension('a', undefined, { pinned: true }); const extension2 = aLocalExtension('b', undefined, { pinned: true }); @@ -1562,100 +1604,14 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); - await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); - assert.strictEqual(testObject.local[0].local?.pinned, true); - assert.strictEqual(testObject.local[1].local?.pinned, true); - assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), []); - assert.equal(instantiationService.get(IConfigurationService).getValue(AutoUpdateConfigurationKey), false); - }); + assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), ['pub.a']); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); - test('Test enable auto update for publisher when auto update mode is onlySelectedExtensions', async () => { - stubConfiguration('onlySelectedExtensions'); + await testObject.updateAutoUpdateValue(true); - const extension1 = aLocalExtension('a', undefined, { pinned: true }); - const extension2 = aLocalExtension('b', undefined, { pinned: true }); - const extension3 = aLocalExtension('a', { publisher: 'pub2' }, { pinned: true }); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2, extension3]); - instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: ILocalExtension, metadata: Partial) => { - local.pinned = !!metadata.pinned; - return local; - }); - testObject = await aWorkbenchService(); - - await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, true); - - assert.strictEqual(testObject.local[0].local?.pinned, false); - assert.strictEqual(testObject.local[1].local?.pinned, false); - assert.strictEqual(testObject.local[2].local?.pinned, true); - assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), ['pub']); - }); - - test('Test disable auto update for publisher when auto update mode is onlySelectedExtensions', async () => { - stubConfiguration('onlySelectedExtensions'); - - const extension1 = aLocalExtension('a', undefined, { pinned: true }); - const extension2 = aLocalExtension('b', undefined, { pinned: true }); - const extension3 = aLocalExtension('a', { publisher: 'pub2' }, { pinned: true }); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2, extension3]); - instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: ILocalExtension, metadata: Partial) => { - local.pinned = !!metadata.pinned; - return local; - }); - testObject = await aWorkbenchService(); - - await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, true); - await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, false); - - assert.strictEqual(testObject.local[0].local?.pinned, true); - assert.strictEqual(testObject.local[1].local?.pinned, true); - assert.strictEqual(testObject.local[2].local?.pinned, true); - assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), []); - }); - - test('Test disable auto update for an extension when auto update for publisher is enabled and update mode is onlySelectedExtensions', async () => { - stubConfiguration('onlySelectedExtensions'); - - const extension1 = aLocalExtension('a', undefined, { pinned: true }); - const extension2 = aLocalExtension('b', undefined, { pinned: true }); - const extension3 = aLocalExtension('a', { publisher: 'pub2' }, { pinned: true }); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2, extension3]); - instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: ILocalExtension, metadata: Partial) => { - local.pinned = !!metadata.pinned; - return local; - }); - testObject = await aWorkbenchService(); - - await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, true); - await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); - - assert.strictEqual(testObject.local[0].local?.pinned, true); - assert.strictEqual(testObject.local[1].local?.pinned, false); - assert.strictEqual(testObject.local[2].local?.pinned, true); - assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), ['pub', '-pub.a']); - }); - - test('Test enable auto update for an extension when auto updates is enabled for publisher and disabled for extension and update mode is onlySelectedExtensions', async () => { - stubConfiguration('onlySelectedExtensions'); - - const extension1 = aLocalExtension('a', undefined, { pinned: true }); - const extension2 = aLocalExtension('b', undefined, { pinned: true }); - const extension3 = aLocalExtension('a', { publisher: 'pub2' }, { pinned: true }); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extension1, extension2, extension3]); - instantiationService.stub(IExtensionManagementService, 'updateMetadata', (local: ILocalExtension, metadata: Partial) => { - local.pinned = !!metadata.pinned; - return local; - }); - testObject = await aWorkbenchService(); - - await testObject.updateAutoUpdateEnablementFor(testObject.local[0].publisher, true); - await testObject.updateAutoUpdateEnablementFor(testObject.local[0], false); - await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); - - assert.strictEqual(testObject.local[0].local?.pinned, false); - assert.strictEqual(testObject.local[1].local?.pinned, false); - assert.strictEqual(testObject.local[2].local?.pinned, true); - assert.deepStrictEqual(testObject.getSelectedExtensionsToAutoUpdate(), ['pub']); + assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); }); async function aWorkbenchService(): Promise { @@ -1669,13 +1625,22 @@ suite('ExtensionsWorkbenchServiceTest', () => { [AutoUpdateConfigurationKey]: autoUpdateValue ?? true, [AutoCheckUpdatesConfigurationKey]: autoCheckUpdatesValue ?? true }; + const emitter = disposableStore.add(new Emitter()); instantiationService.stub(IConfigurationService, { - onDidChangeConfiguration: () => { return undefined!; }, + onDidChangeConfiguration: emitter.event, getValue: (key?: any) => { return key ? values[key] : undefined; }, updateValue: async (key: string, value: any) => { values[key] = value; + emitter.fire({ + affectedKeys: new Set([key]), + source: ConfigurationTarget.USER, + change: { keys: [], overrides: [] }, + affectsConfiguration(configuration, overrides) { + return true; + }, + }); } }); } @@ -1740,7 +1705,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { onDidUpdateExtensionMetadata: Event.None, getInstalled: () => Promise.resolve(installed), installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), - updateMetadata: async (local: Mutable, metadata: Partial) => { + updateMetadata: async (local: Mutable, metadata: Partial, profileLocation: URI) => { local.identifier.uuid = metadata.id; local.publisherDisplayName = metadata.publisherDisplayName!; local.publisherId = metadata.publisherId!; @@ -1748,6 +1713,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { }, getTargetPlatform: async () => getTargetPlatform(platform, arch), async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, + async resetPinnedStateForAllUserExtensions(pinned: boolean) { } }; } }); diff --git a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts index 6ca5f7d48f0..3388ec7baea 100644 --- a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts @@ -138,11 +138,11 @@ export class ExternalTerminalContribution extends Disposable implements IWorkben MenuRegistry.appendMenuItem(MenuId.ExplorerContext, this._openInTerminalMenuItem); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, this._openInIntegratedTerminalMenuItem); - this._configurationService.onDidChangeConfiguration(e => { + this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('terminal.explorerKind') || e.affectsConfiguration('terminal.external')) { this._refreshOpenInTerminalMenuItemTitle(); } - }); + })); this._refreshOpenInTerminalMenuItemTitle(); } diff --git a/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts b/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts index 842599bb6be..5be58d57365 100644 --- a/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts +++ b/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index dd44d65f5e8..1c67fc0f491 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -19,7 +19,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IBulkEditService, ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { IExplorerView, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; -import { IProgressService, ProgressLocation, IProgressNotificationOptions, IProgressCompositeOptions } from 'vs/platform/progress/common/progress'; +import { IProgressService, ProgressLocation, IProgressCompositeOptions, IProgressOptions } from 'vs/platform/progress/common/progress'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -184,12 +184,23 @@ export class ExplorerService implements IExplorerService { async applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string; progressLabel: string; confirmBeforeUndo?: boolean; progressLocation?: ProgressLocation.Explorer | ProgressLocation.Window }): Promise { const cancellationTokenSource = new CancellationTokenSource(); - const promise = this.progressService.withProgress({ - location: options.progressLocation || ProgressLocation.Window, - title: options.progressLabel, - cancellable: edit.length > 1, // Only allow cancellation when there is more than one edit. Since cancelling will not actually stop the current edit that is in progress. - delay: 500, - }, async progress => { + const location = options.progressLocation ?? ProgressLocation.Window; + let progressOptions; + if (location === ProgressLocation.Window) { + progressOptions = { + location: location, + title: options.progressLabel, + cancellable: edit.length > 1, + } satisfies IProgressOptions; + } else { + progressOptions = { + location: location, + title: options.progressLabel, + cancellable: edit.length > 1, + delay: 500, + } satisfies IProgressCompositeOptions; + } + const promise = this.progressService.withProgress(progressOptions, async progress => { await this.bulkEditService.apply(edit, { undoRedoSource: UNDO_REDO_SOURCE, label: options.undoLabel, diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 963e5c56925..b910fb3f2ee 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -10,7 +10,7 @@ import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/commo import { ICommandAction } from 'vs/platform/action/common/action'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { openWindowCommand, newWindowCommand } from 'vs/workbench/contrib/files/browser/fileCommands'; -import { COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, OpenEditorsDirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, OpenEditorsReadonlyEditorContext, OPEN_WITH_EXPLORER_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL, SAVE_ALL_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; +import { COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, OpenEditorsDirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, OpenEditorsReadonlyEditorContext, OPEN_WITH_EXPLORER_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL, SAVE_ALL_COMMAND_ID, OpenEditorsSelectedFileOrUntitledContext } from 'vs/workbench/contrib/files/browser/fileConstants'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -20,7 +20,7 @@ import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOS import { AutoSaveAfterShortDelayContext } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; import { Schemas } from 'vs/base/common/network'; -import { DirtyWorkingCopiesContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, WorkbenchStateContext, WorkspaceFolderCountContext, SidebarFocusContext, ActiveEditorCanRevertContext, ActiveEditorContext, ResourceContextKey, ActiveEditorAvailableEditorIdsContext, MultipleEditorsSelectedInGroupContext, TwoEditorsSelectedInGroupContext } from 'vs/workbench/common/contextkeys'; +import { DirtyWorkingCopiesContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, WorkbenchStateContext, WorkspaceFolderCountContext, SidebarFocusContext, ActiveEditorCanRevertContext, ActiveEditorContext, ResourceContextKey, ActiveEditorAvailableEditorIdsContext, MultipleEditorsSelectedInGroupContext, TwoEditorsSelectedInGroupContext, SelectedEditorsInGroupFileOrUntitledResourceContextKey } from 'vs/workbench/common/contextkeys'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -413,14 +413,14 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: '3_compare', order: 30, command: compareSelectedCommand, - when: ContextKeyExpr.and(ResourceContextKey.HasResource, WorkbenchListDoubleSelection, isFileOrUntitledResourceContextKey) + when: ContextKeyExpr.and(ResourceContextKey.HasResource, WorkbenchListDoubleSelection, OpenEditorsSelectedFileOrUntitledContext) }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { group: '3_compare', order: 30, command: compareSelectedCommand, - when: ContextKeyExpr.and(ResourceContextKey.HasResource, TwoEditorsSelectedInGroupContext, isFileOrUntitledResourceContextKey) + when: ContextKeyExpr.and(ResourceContextKey.HasResource, TwoEditorsSelectedInGroupContext, SelectedEditorsInGroupFileOrUntitledResourceContextKey) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 3e98384f357..58a3df13a9f 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -1110,18 +1110,32 @@ export const pasteFileHandler = async (accessor: ServicesAccessor, fileList?: Fi const configurationService = accessor.get(IConfigurationService); const uriIdentityService = accessor.get(IUriIdentityService); const dialogService = accessor.get(IDialogService); + const hostService = accessor.get(IHostService); const context = explorerService.getContext(false); const hasNativeFilesToPaste = fileList && fileList.length > 0; const confirmPasteNative = hasNativeFilesToPaste && configurationService.getValue('explorer.confirmPasteNative'); - const toPaste = await getFilesToPaste(fileList, clipboardService); + const toPaste = await getFilesToPaste(fileList, clipboardService, hostService); if (confirmPasteNative && toPaste.files.length >= 1) { const message = toPaste.files.length > 1 ? nls.localize('confirmMultiPasteNative', "Are you sure you want to paste the following {0} items?", toPaste.files.length) : nls.localize('confirmPasteNative', "Are you sure you want to paste '{0}'?", basename(toPaste.type === 'paths' ? toPaste.files[0].fsPath : toPaste.files[0].name)); - const detail = toPaste.files.length > 1 ? getFileNamesMessage(toPaste.files.map(item => toPaste.type === 'paths' ? item.path : (item as File).name)) : undefined; + const detail = toPaste.files.length > 1 ? getFileNamesMessage(toPaste.files.map(item => { + if (URI.isUri(item)) { + return item.fsPath; + } + + if (toPaste.type === 'paths') { + const path = hostService.getPathForFile(item); + if (path) { + return path; + } + } + + return item.name; + })) : undefined; const confirmation = await dialogService.confirm({ message, detail, @@ -1270,16 +1284,16 @@ type FilesToPaste = | { type: 'paths'; files: URI[] } | { type: 'data'; files: File[] }; -async function getFilesToPaste(fileList: FileList | undefined, clipboardService: IClipboardService): Promise { +async function getFilesToPaste(fileList: FileList | undefined, clipboardService: IClipboardService, hostService: IHostService): Promise { if (fileList && fileList.length > 0) { // with a `fileList` we support natively pasting file from disk from clipboard - const resources = [...fileList].filter(file => !!file.path && isAbsolute(file.path)).map(file => URI.file(file.path)); + const resources = [...fileList].map(file => hostService.getPathForFile(file)).filter(filePath => !!filePath && isAbsolute(filePath)).map((filePath) => URI.file(filePath!)); if (resources.length) { return { type: 'paths', files: resources, }; } // Support pasting files that we can't read from disk - return { type: 'data', files: [...fileList].filter(file => !file.path) }; + return { type: 'data', files: [...fileList].filter(file => !hostService.getPathForFile(file)) }; } else { // otherwise we fallback to reading resources from our clipboard service return { type: 'paths', files: resources.distinctParents(await clipboardService.readResources(), resource => resource) }; diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 81bf68c3e0b..97fb6ec7c65 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -25,7 +25,7 @@ import { isWeb, isWindows } from 'vs/base/common/platform'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { getResourceForCommand, getMultiSelectedResources, getOpenEditorsViewMultiSelection, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; -import { getMultiSelectedEditorContexts } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { resolveCommandsContext } from 'vs/workbench/browser/parts/editor/editorCommandsContext'; import { Schemas } from 'vs/base/common/network'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -35,7 +35,6 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { basename, joinPath, isEqual } from 'vs/base/common/resources'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { coalesce } from 'vs/base/common/arrays'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -514,13 +513,13 @@ CommandsRegistry.registerCommand({ handler: (accessor, _: URI | object, editorContext: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupsService); - const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService), accessor.get(IEditorGroupsService)); + const resolvedContext = resolveCommandsContext(accessor, [editorContext]); let groups: readonly IEditorGroup[] | undefined = undefined; - if (!contexts.length) { + if (!resolvedContext.groupedEditors.length) { groups = editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); } else { - groups = coalesce(contexts.map(context => editorGroupService.getGroup(context.groupId))); + groups = resolvedContext.groupedEditors.map(({ group }) => group); } return saveDirtyEditorsOfGroups(accessor, groups, { reason: SaveReason.EXPLICIT }); diff --git a/src/vs/workbench/contrib/files/browser/fileConstants.ts b/src/vs/workbench/contrib/files/browser/fileConstants.ts index c38f9987d22..555cff9f3d2 100644 --- a/src/vs/workbench/contrib/files/browser/fileConstants.ts +++ b/src/vs/workbench/contrib/files/browser/fileConstants.ts @@ -35,6 +35,7 @@ export const SAVE_FILES_COMMAND_ID = 'workbench.action.files.saveFiles'; export const OpenEditorsGroupContext = new RawContextKey('groupFocusedInOpenEditors', false); export const OpenEditorsDirtyEditorContext = new RawContextKey('dirtyEditorFocusedInOpenEditors', false); export const OpenEditorsReadonlyEditorContext = new RawContextKey('readonlyEditorFocusedInOpenEditors', false); +export const OpenEditorsSelectedFileOrUntitledContext = new RawContextKey('openEditorsSelectedFileOrUntitled', true); export const ResourceSelectedForCompareContext = new RawContextKey('resourceSelectedForCompare', false); export const REMOVE_ROOT_FOLDER_COMMAND_ID = 'removeRootFolder'; diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 4af58b0bd45..d525ba5860c 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -23,7 +23,7 @@ import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/ import { ILabelService } from 'vs/platform/label/common/label'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExplorerService, UNDO_REDO_SOURCE } from 'vs/workbench/contrib/files/browser/explorerService'; -import { SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/encoding'; +import { GUESSABLE_ENCODINGS, SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/encoding'; import { Schemas } from 'vs/base/common/network'; import { WorkspaceWatcher } from 'vs/workbench/contrib/files/browser/workspaceWatcher'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; @@ -202,6 +202,17 @@ configurationRegistry.registerConfiguration({ 'markdownDescription': nls.localize('autoGuessEncoding', "When enabled, the editor will attempt to guess the character set encoding when opening files. This setting can also be configured per language. Note, this setting is not respected by text search. Only {0} is respected.", '`#files.encoding#`'), 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE }, + 'files.candidateGuessEncodings': { + 'type': 'array', + 'items': { + 'type': 'string', + 'enum': Object.keys(GUESSABLE_ENCODINGS), + 'enumDescriptions': Object.keys(GUESSABLE_ENCODINGS).map(key => GUESSABLE_ENCODINGS[key].labelLong) + }, + 'default': [], + 'markdownDescription': nls.localize('candidateGuessEncodings', "List of character set encodings that the editor should attempt to guess in the order they are listed. In case it cannot be determined, {0} is respected", '`#files.encoding#`'), + 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE + }, 'files.eol': { 'type': 'string', 'enum': [ @@ -270,13 +281,13 @@ configurationRegistry.registerConfiguration({ 'files.autoSaveWorkspaceFilesOnly': { 'type': 'boolean', 'default': false, - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveWorkspaceFilesOnly' }, "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that are inside the opened workspace. Only applies when `#files.autoSave#` is enabled."), + 'markdownDescription': nls.localize('autoSaveWorkspaceFilesOnly', "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that are inside the opened workspace. Only applies when {0} is enabled.", '`#files.autoSave#`'), scope: ConfigurationScope.LANGUAGE_OVERRIDABLE }, 'files.autoSaveWhenNoErrors': { 'type': 'boolean', 'default': false, - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveWhenNoErrors' }, "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that have no errors reported in them at the time the auto save is triggered. Only applies when `#files.autoSave#` is enabled."), + 'markdownDescription': nls.localize('autoSaveWhenNoErrors', "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that have no errors reported in them at the time the auto save is triggered. Only applies when {0} is enabled.", '`#files.autoSave#`'), scope: ConfigurationScope.LANGUAGE_OVERRIDABLE }, 'files.watcherExclude': { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index d8e22884dc8..940bfb82792 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -610,7 +610,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { let anchor = e.anchor; // Adjust for compressed folders (except when mouse is used) - if (anchor instanceof HTMLElement) { + if (DOM.isHTMLElement(anchor)) { if (stat) { const controllers = this.renderer.getCompressedNavigationController(stat); @@ -660,7 +660,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { this.setContextKeys(stat); if (stat) { - const enableTrash = this.configurationService.getValue().files.enableTrash; + const enableTrash = Boolean(this.configurationService.getValue().files?.enableTrash); const hasCapability = this.fileService.hasCapability(stat.resource, FileSystemProviderCapabilities.Trash); this.resourceMoveableToTrash.set(enableTrash && hasCapability); } else { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 068fde51b80..bda9eb9b2aa 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -575,7 +575,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer { } function getIconLabelNameFromHTMLElement(target: HTMLElement | EventTarget | Element | null): { element: HTMLElement; count: number; index: number } | null { - if (!(target instanceof HTMLElement)) { + if (!(DOM.isHTMLElement(target))) { return null; } diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 0fd22533e89..f7560f5a89f 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -18,7 +18,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions'; import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration, OpenEditor } from 'vs/workbench/contrib/files/common/files'; import { CloseAllEditorsAction, CloseEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; -import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { asCssVariable, badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; @@ -28,7 +28,7 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DisposableMap, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { MenuId, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; +import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID, OpenEditorsSelectedFileOrUntitledContext } from 'vs/workbench/contrib/files/browser/fileConstants'; import { ResourceContextKey, MultipleEditorGroupsContext } from 'vs/workbench/common/contextkeys'; import { CodeDataTransfers, containsDragType } from 'vs/platform/dnd/browser/dnd'; import { ResourcesDropHandler, fillEditorsDragData } from 'vs/workbench/browser/dnd'; @@ -55,6 +55,7 @@ import { ILocalizedString } from 'vs/platform/action/common/action'; import { mainWindow } from 'vs/base/browser/window'; import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IFileService } from 'vs/platform/files/common/files'; const $ = dom.$; @@ -74,10 +75,6 @@ export class OpenEditorsView extends ViewPane { private needsRefresh = false; private elements: (OpenEditor | IEditorGroup)[] = []; private sortOrder: 'editorOrder' | 'alphabetical' | 'fullPath'; - private resourceContext!: ResourceContextKey; - private groupFocusedContext!: IContextKey; - private dirtyEditorFocusedContext!: IContextKey; - private readonlyEditorFocusedContext!: IContextKey; private blockFocusActiveEditorTracking = false; constructor( @@ -95,6 +92,7 @@ export class OpenEditorsView extends ViewPane { @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IOpenerService openerService: IOpenerService, + @IFileService private readonly fileService: IFileService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); @@ -245,32 +243,8 @@ export class OpenEditorsView extends ViewPane { this.updateSize(); - // Bind context keys - OpenEditorsFocusedContext.bindTo(this.list.contextKeyService); - ExplorerFocusedContext.bindTo(this.list.contextKeyService); - - this.resourceContext = this.instantiationService.createInstance(ResourceContextKey); - this._register(this.resourceContext); - this.groupFocusedContext = OpenEditorsGroupContext.bindTo(this.contextKeyService); - this.dirtyEditorFocusedContext = OpenEditorsDirtyEditorContext.bindTo(this.contextKeyService); - this.readonlyEditorFocusedContext = OpenEditorsReadonlyEditorContext.bindTo(this.contextKeyService); - + this.handleContextKeys(); this._register(this.list.onContextMenu(e => this.onListContextMenu(e))); - this.list.onDidChangeFocus(e => { - this.resourceContext.reset(); - this.groupFocusedContext.reset(); - this.dirtyEditorFocusedContext.reset(); - this.readonlyEditorFocusedContext.reset(); - const element = e.elements.length ? e.elements[0] : undefined; - if (element instanceof OpenEditor) { - const resource = element.getResource(); - this.dirtyEditorFocusedContext.set(element.editor.isDirty() && !element.editor.isSaving()); - this.readonlyEditorFocusedContext.set(!!element.editor.isReadonly()); - this.resourceContext.set(resource ?? null); - } else if (!!element) { - this.groupFocusedContext.set(true); - } - }); // Open when selecting via keyboard this._register(this.list.onMouseMiddleClick(e => { @@ -318,6 +292,52 @@ export class OpenEditorsView extends ViewPane { })); } + private handleContextKeys() { + if (!this.list) { + return; + } + + // Bind context keys + OpenEditorsFocusedContext.bindTo(this.list.contextKeyService); + ExplorerFocusedContext.bindTo(this.list.contextKeyService); + + const groupFocusedContext = OpenEditorsGroupContext.bindTo(this.contextKeyService); + const dirtyEditorFocusedContext = OpenEditorsDirtyEditorContext.bindTo(this.contextKeyService); + const readonlyEditorFocusedContext = OpenEditorsReadonlyEditorContext.bindTo(this.contextKeyService); + const openEditorsSelectedFileOrUntitledContext = OpenEditorsSelectedFileOrUntitledContext.bindTo(this.contextKeyService); + + const resourceContext = this.instantiationService.createInstance(ResourceContextKey); + this._register(resourceContext); + + this._register(this.list.onDidChangeFocus(e => { + resourceContext.reset(); + groupFocusedContext.reset(); + dirtyEditorFocusedContext.reset(); + readonlyEditorFocusedContext.reset(); + + const element = e.elements.length ? e.elements[0] : undefined; + if (element instanceof OpenEditor) { + const resource = element.getResource(); + dirtyEditorFocusedContext.set(element.editor.isDirty() && !element.editor.isSaving()); + readonlyEditorFocusedContext.set(!!element.editor.isReadonly()); + resourceContext.set(resource ?? null); + } else if (!!element) { + groupFocusedContext.set(true); + } + })); + + this._register(this.list.onDidChangeSelection(e => { + const selectedAreFileOrUntitled = e.elements.every(e => { + if (e instanceof OpenEditor) { + const resource = e.getResource(); + return resource && (resource.scheme === Schemas.untitled || this.fileService.hasProvider(resource)); + } + return false; + }); + openEditorsSelectedFileOrUntitledContext.set(selectedAreFileOrUntitled); + })); + } + override focus(): void { super.focus(); diff --git a/src/vs/workbench/contrib/files/browser/workspaceWatcher.ts b/src/vs/workbench/contrib/files/browser/workspaceWatcher.ts index 17ac3761876..3cba8364293 100644 --- a/src/vs/workbench/contrib/files/browser/workspaceWatcher.ts +++ b/src/vs/workbench/contrib/files/browser/workspaceWatcher.ts @@ -115,7 +115,8 @@ export class WorkspaceWatcher extends Disposable { reason = 'ETERM'; } - // Log telemetry if we gathered a reason (TODO@bpasero remove me once the TS experiment is over) + // Log telemetry if we gathered a reason (logging it from the renderer + // allows us to investigate this situation in context of experiments) if (reason) { type WatchErrorClassification = { owner: 'bpasero'; diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index f104ba762eb..75b57831d84 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts b/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts index 2c08f73b54c..66481ab3a23 100644 --- a/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { PreTrie, ExplorerFileNestingTrie, SufTrie } from 'vs/workbench/contrib/files/common/explorerFileNestingTrie'; -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; const fakeFilenameAttributes = { dirname: 'mydir', basename: '', extname: '' }; diff --git a/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts b/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts index 938fed848a2..13870699b3c 100644 --- a/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/explorerModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { isLinux, isWindows, OS } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; diff --git a/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts b/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts index cea1d9c7172..ea30440fa54 100644 --- a/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts b/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts index e1e8db24108..f5070edb179 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { incrementFileName } from 'vs/workbench/contrib/files/browser/fileActions'; diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index ef6adefcdd2..1675b2d4571 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; diff --git a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts index ca865ea076c..94f9a716751 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index 75e6c3b5cdc..f3d6c8fb198 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { TextFileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/textFileEditorTracker'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 4ab19a40890..7d3e4041271 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { IMenuItem, isIMenuItem, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import * as InlineChatActions from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; -import { INLINE_CHAT_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_CONFIG_TXT_BTNS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, INLINE_CHAT_ID, InlineChatConfigKeys, MENU_INLINE_CHAT_CONTENT_STATUS, MENU_INLINE_CHAT_EXECUTE, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -19,6 +19,12 @@ import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browse import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; import { InlineChatEnabler, InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { CancelAction, SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { localize } from 'vs/nls'; +import { CONTEXT_CHAT_INPUT_HAS_TEXT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; // --- browser @@ -28,6 +34,41 @@ registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, Instant registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors +// --- MENU special --- + +const sendActionMenuItem: IMenuItem = { + group: '0_main', + order: 0, + command: { + id: SubmitAction.ID, + title: localize('edit', "Send"), + }, + when: ContextKeyExpr.and( + CONTEXT_CHAT_INPUT_HAS_TEXT, + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), + CTX_INLINE_CHAT_CONFIG_TXT_BTNS + ), +}; + +MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_CONTENT_STATUS, sendActionMenuItem); +MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, sendActionMenuItem); + +const cancelActionMenuItem: IMenuItem = { + group: '0_main', + order: 0, + command: { + id: CancelAction.ID, + title: localize('cancel', "Stop Request"), + shortTitle: localize('cancelShort', "Stop"), + }, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, + ), +}; + +MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, cancelActionMenuItem); + +// --- actions --- registerAction2(InlineChatActions.StartSessionAction); registerAction2(InlineChatActions.CloseAction); @@ -36,7 +77,6 @@ registerAction2(InlineChatActions.UnstashSessionAction); registerAction2(InlineChatActions.DiscardHunkAction); registerAction2(InlineChatActions.DiscardAction); registerAction2(InlineChatActions.RerunAction); -registerAction2(InlineChatActions.CancelSessionAction); registerAction2(InlineChatActions.MoveToNextHunk); registerAction2(InlineChatActions.MoveToPreviousHunk); @@ -56,3 +96,47 @@ workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookC registerWorkbenchContribution2(InlineChatEnabler.Id, InlineChatEnabler, WorkbenchPhase.AfterRestored); AccessibleViewRegistry.register(new InlineChatAccessibleView()); + + +// MARK - Menu Copier +// menu copier that we use for text-button mode. +// When active it filters out the send and cancel actions from the chat menu +class MenuCopier implements IDisposable { + + static Id = 'inlineChat.menuCopier'; + + readonly dispose: () => void; + + constructor(@IConfigurationService configService: IConfigurationService,) { + + const store = new DisposableStore(); + function updateMenu() { + store.clear(); + for (const item of MenuRegistry.getMenuItems(MenuId.ChatExecute)) { + if (configService.getValue(InlineChatConfigKeys.ExpTextButtons) && isIMenuItem(item) && (item.command.id === SubmitAction.ID || item.command.id === CancelAction.ID)) { + continue; + } + store.add(MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_EXECUTE, item)); + } + } + updateMenu(); + const listener = MenuRegistry.onDidChangeMenu(e => { + if (e.has(MenuId.ChatExecute)) { + updateMenu(); + } + }); + const listener2 = configService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(InlineChatConfigKeys.ExpTextButtons)) { + updateMenu(); + } + }); + + this.dispose = () => { + listener.dispose(); + listener2.dispose(); + store.dispose(); + }; + } +} + +registerWorkbenchContribution2(MenuCopier.Id, MenuCopier, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts index d168f54f836..a2348928fb6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts @@ -23,4 +23,5 @@ export class InlineChatAccessibilityHelp implements IAccessibleViewImplentation } return getChatAccessibilityHelpProvider(accessor, codeEditor, 'inlineChat'); } + dispose() { } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts index 4cac306c23f..797dae2825d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts @@ -11,6 +11,8 @@ import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/access import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; export class InlineChatAccessibleView implements IAccessibleViewImplentation { readonly priority = 100; @@ -35,11 +37,12 @@ export class InlineChatAccessibleView implements IAccessibleViewImplentation { return { id: AccessibleViewProviderId.InlineChat, verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, - provideContent(): string { return responseContent; }, + provideContent(): string { return renderMarkdownAsPlaintext(new MarkdownString(responseContent), true); }, onClose() { controller.focus(); }, options: { type: AccessibleViewType.View } }; } + dispose() { } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index ce5b514ede3..2009001a7ea 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/em import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_HAS_STASHED_SESSION, ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF, ACTION_REGENERATE_RESPONSE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, MENU_INLINE_CHAT_CONTENT_STATUS, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize, localize2 } from 'vs/nls'; import { Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -29,7 +29,6 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { CONTEXT_CHAT_REQUEST_IN_PROGRESS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -55,7 +54,7 @@ export class StartSessionAction extends EditorAction2 { title: LOCALIZED_START_INLINE_CHAT_STRING, category: AbstractInlineChatAction.category, f1: true, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_PROVIDER, EditorContextKeys.writable), + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT, EditorContextKeys.writable), keybinding: { when: EditorContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib, @@ -80,7 +79,7 @@ export class StartSessionAction extends EditorAction2 { let options: InlineChatRunOptions | undefined; const arg = _args[0]; - if (arg && InlineChatRunOptions.isInteractiveEditorOptions(arg)) { + if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { options = arg; } InlineChatController.get(editor)?.run({ ...options }); @@ -123,7 +122,7 @@ export abstract class AbstractInlineChatAction extends EditorAction2 { super({ ...desc, category: AbstractInlineChatAction.category, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_PROVIDER, desc.precondition) + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT, desc.precondition) }); } @@ -228,27 +227,6 @@ export class FocusInlineChat extends EditorAction2 { } } -export class DiscardHunkAction extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineChat.discardHunkChange', - title: localize('discard', 'Discard'), - icon: Codicon.clearAll, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty), CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live)), - group: '0_main', - order: 3 - } - }); - } - - async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - return ctrl.discardHunk(); - } -} export class DiscardAction extends AbstractInlineChatAction { @@ -271,33 +249,6 @@ export class DiscardAction extends AbstractInlineChatAction { } } -export class ToggleDiffForChange extends AbstractInlineChatAction { - - constructor() { - super({ - id: ACTION_TOGGLE_DIFF, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), - title: localize2('showChanges', 'Toggle Changes'), - icon: Codicon.diffSingle, - toggled: { - condition: CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, - }, - menu: [ - { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '1_main', - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), - order: 10, - } - ] - }); - } - - override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController): void { - ctrl.toggleDiff(); - } -} - export class AcceptChanges extends AbstractInlineChatAction { constructor() { @@ -313,10 +264,13 @@ export class AcceptChanges extends AbstractInlineChatAction { primary: KeyMod.CtrlCmd | KeyCode.Enter, }], menu: { - when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), id: MENU_INLINE_CHAT_WIDGET_STATUS, group: '0_main', - order: 0 + order: 1, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), + CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits) + ), } }); } @@ -326,32 +280,72 @@ export class AcceptChanges extends AbstractInlineChatAction { } } -export class CancelSessionAction extends AbstractInlineChatAction { +export class DiscardHunkAction extends AbstractInlineChatAction { constructor() { super({ - id: 'inlineChat.cancel', - title: localize('cancel', 'Cancel'), + id: 'inlineChat.discardHunkChange', + title: localize('discard', 'Discard'), icon: Codicon.clearAll, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Preview)), - keybinding: { - weight: KeybindingWeight.EditorContrib - 1, - primary: KeyCode.Escape - }, + precondition: CTX_INLINE_CHAT_VISIBLE, menu: { id: MENU_INLINE_CHAT_WIDGET_STATUS, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Preview), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), group: '0_main', - order: 3 + order: 2, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), + CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits), + CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live) + ), + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape, + when: CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits) } }); } async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - ctrl.cancelSession(); + return ctrl.discardHunk(); } } +export class RerunAction extends AbstractInlineChatAction { + constructor() { + super({ + id: ACTION_REGENERATE_RESPONSE, + title: localize2('chat.rerun.label', "Rerun Request"), + shortTitle: localize('rerun', 'Rerun'), + f1: false, + icon: Codicon.refresh, + precondition: CTX_INLINE_CHAT_VISIBLE, + menu: { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: '0_main', + order: 5, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), + CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.None) + ) + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyR + } + }); + } + + override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { + const chatService = accessor.get(IChatService); + const model = ctrl.chatWidget.viewModel?.model; + + const lastRequest = model?.getRequests().at(-1); + if (lastRequest) { + await chatService.resendRequest(lastRequest, { noCommandDetection: false, attempt: lastRequest.attempt + 1, location: ctrl.chatWidget.location }); + } + } +} export class CloseAction extends AbstractInlineChatAction { @@ -362,15 +356,25 @@ export class CloseAction extends AbstractInlineChatAction { icon: Codicon.close, precondition: CTX_INLINE_CHAT_VISIBLE, keybinding: { - weight: KeybindingWeight.EditorContrib - 1, + weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Escape, - when: CTX_INLINE_CHAT_USER_DID_EDIT.negate() }, - menu: { - id: MENU_INLINE_CHAT_WIDGET, - group: 'navigation', + menu: [{ + id: MENU_INLINE_CHAT_CONTENT_STATUS, + group: '0_main', order: 10, - } + }, { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), + ContextKeyExpr.or( + CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages), + CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Preview) + ) + ), + }] }); } @@ -383,13 +387,14 @@ export class ConfigureInlineChatAction extends AbstractInlineChatAction { constructor() { super({ id: 'inlineChat.configure', - title: localize('configure', 'Configure '), + title: localize2('configure', 'Configure Inline Chat'), icon: Codicon.settingsGear, precondition: CTX_INLINE_CHAT_VISIBLE, + f1: true, menu: { - id: MENU_INLINE_CHAT_WIDGET, - group: 'config', - order: 1, + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: 'zzz', + order: 5 } }); } @@ -482,10 +487,23 @@ export class ViewInChatAction extends AbstractInlineChatAction { title: localize('viewInChat', 'View in Chat'), icon: Codicon.commentDiscussion, precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET, - group: 'navigation', - order: 5 + menu: [{ + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: 'more', + order: 1, + when: CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.Messages) + }, { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages), + CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() + ) + }], + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, } }); } @@ -494,29 +512,27 @@ export class ViewInChatAction extends AbstractInlineChatAction { } } -export class RerunAction extends AbstractInlineChatAction { +export class ToggleDiffForChange extends AbstractInlineChatAction { + constructor() { super({ - id: ACTION_REGENERATE_RESPONSE, - title: localize2('chat.rerun.label', "Rerun Request"), - f1: false, - icon: Codicon.refresh, - precondition: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), + id: ACTION_TOGGLE_DIFF, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), + title: localize2('showChanges', 'Toggle Changes'), + icon: Codicon.diffSingle, + toggled: { + condition: CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, + }, menu: { id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 5, + group: 'zzz', + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live)), + order: 1, } }); } - override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - const chatService = accessor.get(IChatService); - const model = ctrl.chatWidget.viewModel?.model; - - const lastRequest = model?.getRequests().at(-1); - if (lastRequest) { - await chatService.resendRequest(lastRequest, { noCommandDetection: false, attempt: lastRequest.attempt + 1, location: ctrl.chatWidget.location }); - } + override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController): void { + ctrl.toggleDiff(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts index b68ae908369..92ba84eb48e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -10,11 +10,10 @@ import { IDimension } from 'vs/editor/common/core/dimension'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IPosition, Position } from 'vs/editor/common/core/position'; -import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { inlineChatBackground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { inlineChatBackground, InlineChatConfigKeys, MENU_INLINE_CHAT_CONTENT_STATUS, MENU_INLINE_CHAT_EXECUTE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { ChatWidget, IChatWidgetLocationOptions } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; @@ -23,6 +22,10 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ScrollType } from 'vs/editor/common/editorCommon'; +import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { TextOnlyMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class InlineChatContentWidget implements IContentWidget { @@ -32,7 +35,7 @@ export class InlineChatContentWidget implements IContentWidget { private readonly _store = new DisposableStore(); private readonly _domNode = document.createElement('div'); private readonly _inputContainer = document.createElement('div'); - private readonly _messageContainer = document.createElement('div'); + private readonly _toolbarContainer = document.createElement('div'); private _position?: IPosition; @@ -46,9 +49,11 @@ export class InlineChatContentWidget implements IContentWidget { private readonly _widget: ChatWidget; constructor( + location: IChatWidgetLocationOptions, private readonly _editor: ICodeEditor, @IInstantiationService instaService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService configurationService: IConfigurationService ) { this._defaultChatModel = this._store.add(instaService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); @@ -63,17 +68,18 @@ export class InlineChatContentWidget implements IContentWidget { this._widget = scopedInstaService.createInstance( ChatWidget, - ChatAgentLocation.Editor, + location, { resource: true }, { defaultElementHeight: 32, editorOverflowWidgetsDomNode: _editor.getOverflowWidgetsDomNode(), - renderStyle: 'compact', + renderStyle: 'minimal', renderInputOnTop: true, renderFollowups: true, supportsFileReferences: false, menus: { - telemetrySource: 'inlineChat-content' + telemetrySource: 'inlineChat-content', + executeToolbar: MENU_INLINE_CHAT_EXECUTE, }, filter: _item => false }, @@ -87,22 +93,34 @@ export class InlineChatContentWidget implements IContentWidget { this._store.add(this._widget); this._widget.render(this._inputContainer); this._widget.setModel(this._defaultChatModel, {}); - this._store.add(this._widget.inputEditor.onDidContentSizeChange(() => _editor.layoutContentWidget(this))); + this._store.add(this._widget.onDidChangeContentHeight(() => _editor.layoutContentWidget(this))); this._domNode.tabIndex = -1; this._domNode.className = 'inline-chat-content-widget interactive-session'; this._domNode.appendChild(this._inputContainer); - this._messageContainer.classList.add('hidden', 'message'); - this._domNode.appendChild(this._messageContainer); + this._toolbarContainer.classList.add('toolbar'); + if (configurationService.getValue(InlineChatConfigKeys.ExpTextButtons)) { + this._toolbarContainer.style.display = 'inherit'; + this._domNode.style.paddingBottom = '4px'; + } + this._domNode.appendChild(this._toolbarContainer); + const toolbar = this._store.add(scopedInstaService.createInstance(MenuWorkbenchToolBar, this._toolbarContainer, MENU_INLINE_CHAT_CONTENT_STATUS, { + actionViewItemProvider: action => action instanceof MenuItemAction ? instaService.createInstance(TextOnlyMenuEntryActionViewItem, action, { conversational: true }) : undefined, + toolbarOptions: { primaryGroup: '0_main' }, + icon: false, + label: true, + })); + + this._store.add(toolbar.onDidChangeMenuItems(() => { + this._domNode.classList.toggle('contents', toolbar.getItemsLength() > 1); + })); const tracker = dom.trackFocus(this._domNode); this._store.add(tracker.onDidBlur(() => { - if (this._visible - // && !"ON" - ) { + if (this._visible && this._widget.inputEditor.getModel()?.getValueLength() === 0) { this._onDidBlur.fire(); } })); @@ -136,7 +154,7 @@ export class InlineChatContentWidget implements IContentWidget { const maxHeight = this._widget.input.inputEditor.getOption(EditorOption.lineHeight) * 5; const inputEditorHeight = this._widget.contentHeight; - this._widget.layout(Math.min(maxHeight, inputEditorHeight), 360); + this._widget.layout(Math.min(maxHeight, inputEditorHeight), 390); // const actualHeight = this._widget.inputPartHeight; // return new dom.Dimension(width, actualHeight); @@ -170,7 +188,6 @@ export class InlineChatContentWidget implements IContentWidget { this._focusNext = true; this._editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(position), ScrollType.Immediate); - this._widget.inputEditor.setValue(''); const wordInfo = this._editor.getModel()?.getWordAtPosition(position); @@ -184,6 +201,7 @@ export class InlineChatContentWidget implements IContentWidget { if (this._visible) { this._visible = false; this._editor.removeContentWidget(this); + this._widget.inputEditor.setValue(''); this._widget.saveState(); this._widget.setVisible(false); } @@ -191,16 +209,6 @@ export class InlineChatContentWidget implements IContentWidget { setSession(session: Session): void { this._widget.setModel(session.chatModel, {}); - this._widget.setInputPlaceholder(session.session.placeholder ?? ''); - this._updateMessage(session.session.message ?? ''); - } - - private _updateMessage(message: string) { - if (message) { - const renderedMessage = renderLabelWithIcons(message); - dom.reset(this._messageContainer, ...renderedMessage); - } - this._messageContainer.style.display = message ? 'inherit' : 'none'; - this._editor.layoutContentWidget(this); + this._widget.setInputPlaceholder(session.agent.description ?? ''); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index e5794444295..782bc3b95bc 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -18,8 +18,8 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; -import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; -import { CompletionItemKind, CompletionList, TextEdit } from 'vs/editor/common/languages'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { TextEdit } from 'vs/editor/common/languages'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { localize } from 'vs/nls'; @@ -28,24 +28,27 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IChatWidgetService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; +import { showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IInlineChatSavingService } from './inlineChatSavingService'; import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IInlineChatSessionService } from './inlineChatSessionService'; import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { InlineChatZoneWidget } from './inlineChatZoneWidget'; -import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { StashedSession } from './inlineChatSession'; -import { IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model'; +import { IValidEditOperation } from 'vs/editor/common/model'; import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; -import { ChatModel, IChatRequestModel, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { InlineChatError } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; -import { isEqual } from 'vs/base/common/resources'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; +import { generateUuid } from 'vs/base/common/uuid'; +import { isEqual } from 'vs/base/common/resources'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { IChatWidgetLocationOptions } from 'vs/workbench/contrib/chat/browser/chatWidget'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -78,7 +81,7 @@ export abstract class InlineChatRunOptions { position?: IPosition; withIntentDetection?: boolean; - static isInteractiveEditorOptions(options: any): options is InlineChatRunOptions { + static isInlineChatRunOptions(options: any): options is InlineChatRunOptions { const { initialSelection, initialRange, message, autoSend, position, existingSession } = options; if ( typeof message !== 'undefined' && typeof message !== 'string' @@ -102,15 +105,13 @@ export class InlineChatController implements IEditorContribution { private _isDisposed: boolean = false; private readonly _store = new DisposableStore(); - private readonly _input: Lazy; - private readonly _zone: Lazy; + + private readonly _ui: Lazy<{ content: InlineChatContentWidget; zone: InlineChatZoneWidget }>; private readonly _ctxVisible: IContextKey; - private readonly _ctxResponseTypes: IContextKey; - private readonly _ctxDidEdit: IContextKey; + private readonly _ctxResponseType: IContextKey; private readonly _ctxUserDidEdit: IContextKey; - private readonly _ctxLastFeedbackKind: IContextKey<'helpful' | 'unhelpful' | ''>; - private readonly _ctxSupportIssueReporting: IContextKey; + private readonly _ctxRequestInProgress: IContextKey; private _messages = this._store.add(new Emitter()); @@ -118,10 +119,10 @@ export class InlineChatController implements IEditorContribution { readonly onWillStartSession = this._onWillStartSession.event; get chatWidget() { - if (this._input.value.isVisible) { - return this._input.value.chatWidget; + if (this._ui.value.content.isVisible) { + return this._ui.value.content.chatWidget; } else { - return this._zone.value.widget.chatWidget; + return this._ui.value.zone.widget.chatWidget; } } @@ -141,18 +142,46 @@ export class InlineChatController implements IEditorContribution { @IDialogService private readonly _dialogService: IDialogService, @IContextKeyService contextKeyService: IContextKeyService, @IChatService private readonly _chatService: IChatService, - @ILanguageFeaturesService private readonly _languageFeatureService: ILanguageFeaturesService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @INotebookEditorService notebookEditorService: INotebookEditorService, ) { this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); - this._ctxDidEdit = CTX_INLINE_CHAT_DID_EDIT.bindTo(contextKeyService); this._ctxUserDidEdit = CTX_INLINE_CHAT_USER_DID_EDIT.bindTo(contextKeyService); - this._ctxResponseTypes = CTX_INLINE_CHAT_RESPONSE_TYPES.bindTo(contextKeyService); - this._ctxLastFeedbackKind = CTX_INLINE_CHAT_LAST_FEEDBACK.bindTo(contextKeyService); - this._ctxSupportIssueReporting = CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING.bindTo(contextKeyService); + this._ctxResponseType = CTX_INLINE_CHAT_RESPONSE_TYPE.bindTo(contextKeyService); + this._ctxRequestInProgress = CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); - this._input = new Lazy(() => this._store.add(_instaService.createInstance(InlineChatContentWidget, this._editor))); - this._zone = new Lazy(() => this._store.add(_instaService.createInstance(InlineChatZoneWidget, this._editor))); + this._ui = new Lazy(() => { + + const location: IChatWidgetLocationOptions = { + location: ChatAgentLocation.Editor, + resolveData: () => { + assertType(this._editor.hasModel()); + assertType(this._session); + return { + type: ChatAgentLocation.Editor, + selection: this._editor.getSelection(), + document: this._session.textModelN.uri, + wholeRange: this._session?.wholeRange.trackedInitialRange, + }; + } + }; + + // inline chat in notebooks + // check if this editor is part of a notebook editor + // and iff so, use the notebook location but keep the resolveData + // talk about editor data + for (const notebookEditor of notebookEditorService.listNotebookEditors()) { + for (const [, codeEditor] of notebookEditor.codeEditors) { + if (codeEditor === this._editor) { + location.location = ChatAgentLocation.Notebook; + break; + } + } + } + + const content = this._store.add(_instaService.createInstance(InlineChatContentWidget, location, this._editor)); + const zone = this._store.add(_instaService.createInstance(InlineChatZoneWidget, location, this._editor)); + return { content, zone }; + }); this._store.add(this._editor.onDidChangeModel(async e => { if (this._session || !e.newModelUrl) { @@ -208,7 +237,7 @@ export class InlineChatController implements IEditorContribution { } getMessage(): string | undefined { - return this._zone.value.widget.responseContent; + return this._ui.value.zone.widget.responseContent; } getId(): string { @@ -220,7 +249,7 @@ export class InlineChatController implements IEditorContribution { } getWidgetPosition(): Position | undefined { - return this._zone.value.position; + return this._ui.value.zone.position; } private _currentRun?: Promise; @@ -287,8 +316,7 @@ export class InlineChatController implements IEditorContribution { if (m === Message.ACCEPT_INPUT) { // user accepted the input before having a session options.autoSend = true; - this._zone.value.widget.updateProgress(true); - this._zone.value.widget.updateInfo(localize('welcome.2', "Getting ready...")); + this._ui.value.zone.widget.updateInfo(localize('welcome.2', "Getting ready...")); } else { createSessionCts.cancel(); } @@ -332,11 +360,11 @@ export class InlineChatController implements IEditorContribution { // create a new strategy switch (session.editMode) { case EditMode.Preview: - this._strategy = this._instaService.createInstance(PreviewStrategy, session, this._editor, this._zone.value); + this._strategy = this._instaService.createInstance(PreviewStrategy, session, this._editor, this._ui.value.zone); break; case EditMode.Live: default: - this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._zone.value); + this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._ui.value.zone); break; } @@ -362,17 +390,14 @@ export class InlineChatController implements IEditorContribution { this._sessionStore.add(this._session.wholeRange.onDidChange(updateWholeRangeDecoration)); updateWholeRangeDecoration(); - this._sessionStore.add(this._input.value.onDidBlur(() => this.cancelSession())); + this._sessionStore.add(this._ui.value.content.onDidBlur(() => this.cancelSession())); - this._input.value.setSession(this._session); - // this._zone.value.widget.updateSlashCommands(this._session.session.slashCommands ?? []); + this._ui.value.content.setSession(this._session); + // this._ui.value.zone.widget.updateSlashCommands(this._session.session.slashCommands ?? []); this._updatePlaceholder(); - const message = this._session.session.message ?? localize('welcome.1', "AI-generated code may be incorrect"); - - - this._zone.value.widget.updateInfo(message); this._showWidget(!this._session.chatModel.hasRequests); + this._ui.value.zone.widget.updateToolbar(true); this._sessionStore.add(this._editor.onDidChangeModel((e) => { const msg = this._session?.chatModel.hasRequests @@ -411,114 +436,14 @@ export class InlineChatController implements IEditorContribution { })); this._sessionStore.add(this._session.chatModel.onDidChange(async e => { - if (e.kind === 'addRequest' && e.request.response) { - this._zone.value.widget.updateProgress(true); - - const listener = e.request.response.onDidChange(() => { - - if (e.request.response?.isCanceled || e.request.response?.isComplete) { - this._zone.value.widget.updateProgress(false); - listener.dispose(); - } - }); - } else if (e.kind === 'removeRequest') { - // TODO@jrieken this currently is buggy when removing not the very last request/response - // if (this._session!.lastExchange?.response instanceof ReplyResponse) { - // try { - // this._session!.hunkData.ignoreTextModelNChanges = true; - // await this._strategy!.undoChanges(this._session!.lastExchange.response.modelAltVersionId); - // } finally { - // this._session!.hunkData.ignoreTextModelNChanges = false; - // } - // } + if (e.kind === 'removeRequest') { + // TODO@jrieken there is still some work left for when a request "in the middle" + // is removed. We will undo all changes till that point but not remove those + // later request + await this._session!.undoChangesUntil(e.requestId); } })); - // Update context key - this._ctxSupportIssueReporting.set(this._session.agent.metadata.supportIssueReporting ?? false); - - // #region DEBT - // DEBT@jrieken - // REMOVE when agents are adopted - this._sessionStore.add(this._languageFeatureService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'inline chat commands', - triggerCharacters: ['/'], - provideCompletionItems: (model, position, context, token) => { - if (position.lineNumber !== 1) { - return undefined; - } - if (!this._session || !this._session.session.slashCommands) { - return undefined; - } - const widget = this._chatWidgetService.getWidgetByInputUri(model.uri); - if (widget !== this._zone.value.widget.chatWidget && widget !== this._input.value.chatWidget) { - return undefined; - } - - const result: CompletionList = { suggestions: [], incomplete: false }; - for (const command of this._session.session.slashCommands) { - const withSlash = `/${command.name}`; - result.suggestions.push({ - label: { label: withSlash, description: command.description ?? '' }, - kind: CompletionItemKind.Text, - insertText: withSlash, - range: Range.fromPositions(new Position(1, 1), position), - }); - } - - return result; - } - })); - - const updateSlashDecorations = (collection: IEditorDecorationsCollection, model: ITextModel) => { - - const newDecorations: IModelDeltaDecoration[] = []; - for (const command of (this._session?.session.slashCommands ?? []).sort((a, b) => b.name.length - a.name.length)) { - const withSlash = `/${command.name}`; - const firstLine = model.getLineContent(1); - if (firstLine.startsWith(withSlash)) { - newDecorations.push({ - range: new Range(1, 1, 1, withSlash.length + 1), - options: { - description: 'inline-chat-slash-command', - inlineClassName: 'inline-chat-slash-command', - after: { - // Force some space between slash command and placeholder - content: ' ' - } - } - }); - - // inject detail when otherwise empty - if (firstLine.trim() === `/${command.name}`) { - newDecorations.push({ - range: new Range(1, withSlash.length, 1, withSlash.length), - options: { - description: 'inline-chat-slash-command-detail', - after: { - content: `${command.description}`, - inlineClassName: 'inline-chat-slash-command-detail' - } - } - }); - } - break; - } - } - collection.set(newDecorations); - }; - const inputInputEditor = this._input.value.chatWidget.inputEditor; - const zoneInputEditor = this._zone.value.widget.chatWidget.inputEditor; - const inputDecorations = inputInputEditor.createDecorationsCollection(); - const zoneDecorations = zoneInputEditor.createDecorationsCollection(); - this._sessionStore.add(inputInputEditor.onDidChangeModelContent(() => updateSlashDecorations(inputDecorations, inputInputEditor.getModel()!))); - this._sessionStore.add(zoneInputEditor.onDidChangeModelContent(() => updateSlashDecorations(zoneDecorations, zoneInputEditor.getModel()!))); - this._sessionStore.add(toDisposable(() => { - inputDecorations.clear(); - zoneDecorations.clear(); - })); - - //#endregion ------- DEBT if (!this._session.chatModel.hasRequests) { return State.WAIT_FOR_INPUT; @@ -566,7 +491,7 @@ export class InlineChatController implements IEditorContribution { if (options.autoSend) { delete options.autoSend; this._showWidget(false); - this._zone.value.widget.chatWidget.acceptInput(); + this._ui.value.zone.widget.chatWidget.acceptInput(); } await barrier.wait(); @@ -582,7 +507,7 @@ export class InlineChatController implements IEditorContribution { } if (message & Message.ACCEPT_SESSION) { - this._zone.value.widget.selectAll(false); + this._ui.value.zone.widget.selectAll(false); return State.ACCEPT; } @@ -590,10 +515,7 @@ export class InlineChatController implements IEditorContribution { return State.WAIT_FOR_INPUT; } - const input = request.message.text; - this._zone.value.widget.value = input; - - this._session.addInput(new SessionPrompt(request)); + this._session.addInput(new SessionPrompt(request, this._editor.getModel()!.getAlternativeVersionId())); return State.SHOW_REQUEST; } @@ -603,6 +525,8 @@ export class InlineChatController implements IEditorContribution { assertType(this._session); assertType(this._session.chatModel.requestInProgress); + this._ctxRequestInProgress.set(true); + const { chatModel } = this._session; const request: IChatRequestModel | undefined = chatModel.getRequests().at(-1); @@ -610,9 +534,9 @@ export class InlineChatController implements IEditorContribution { assertType(request.response); this._showWidget(false); - this._zone.value.widget.value = request.message.text; - this._zone.value.widget.selectAll(false); - this._zone.value.widget.updateInfo(''); + // this._ui.value.zone.widget.value = request.message.text; + this._ui.value.zone.widget.selectAll(false); + this._ui.value.zone.widget.updateInfo(''); const { response } = request; const responsePromise = new DeferredPromise(); @@ -624,7 +548,7 @@ export class InlineChatController implements IEditorContribution { const progressiveEditsClock = StopWatch.create(); const progressiveEditsQueue = new Queue(); - let next: State.SHOW_RESPONSE | State.CANCEL | State.PAUSE | State.ACCEPT | State.WAIT_FOR_INPUT = State.SHOW_RESPONSE; + let next: State.SHOW_RESPONSE | State.SHOW_REQUEST | State.CANCEL | State.PAUSE | State.ACCEPT | State.WAIT_FOR_INPUT = State.SHOW_RESPONSE; store.add(Event.once(this._messages.event)(message => { this._log('state=_makeRequest) message received', message); this._chatService.cancelCurrentRequestForSession(chatModel.sessionId); @@ -641,89 +565,104 @@ export class InlineChatController implements IEditorContribution { if (e.kind === 'removeRequest' && e.requestId === request.id) { progressiveEditsCts.cancel(); responsePromise.complete(); - next = State.CANCEL; + if (e.reason === ChatRequestRemovalReason.Resend) { + next = State.SHOW_REQUEST; + } else { + next = State.CANCEL; + } } })); // cancel the request when the user types - store.add(this._zone.value.widget.chatWidget.inputEditor.onDidChangeModelContent(() => { + store.add(this._ui.value.zone.widget.chatWidget.inputEditor.onDidChangeModelContent(() => { this._chatService.cancelCurrentRequestForSession(chatModel.sessionId); })); let lastLength = 0; let isFirstChange = true; + const sha1 = new DefaultModelSHA1Computer(); + const textModel0Sha1 = sha1.canComputeSHA1(this._session.textModel0) + ? sha1.computeSHA1(this._session.textModel0) + : generateUuid(); + const editState: IChatTextEditGroupState = { sha1: textModel0Sha1, applied: 0 }; + let localEditGroup: IChatTextEditGroup | undefined; + // apply edits - store.add(response.onDidChange(() => { + const handleResponse = () => { + + this._updateCtxResponseType(); + + if (!localEditGroup) { + localEditGroup = response.response.value.find(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)); + } + + if (localEditGroup) { + + localEditGroup.state ??= editState; + + const edits = localEditGroup.edits; + const newEdits = edits.slice(lastLength); + if (newEdits.length > 0) { + // NEW changes + lastLength = edits.length; + progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); + progressiveEditsClock.reset(); + + progressiveEditsQueue.queue(async () => { + + const startThen = this._session!.wholeRange.value.getStartPosition(); + + // making changes goes into a queue because otherwise the async-progress time will + // influence the time it takes to receive the changes and progressive typing will + // become infinitely fast + for (const edits of newEdits) { + await this._makeChanges(edits, { + duration: progressiveEditsAvgDuration.value, + token: progressiveEditsCts.token + }, isFirstChange); + + isFirstChange = false; + } + + // reshow the widget if the start position changed or shows at the wrong position + const startNow = this._session!.wholeRange.value.getStartPosition(); + if (!startNow.equals(startThen) || !this._ui.value.zone.position?.equals(startNow)) { + this._showWidget(false, startNow.delta(-1)); + } + }); + } + } if (response.isCanceled) { progressiveEditsCts.cancel(); responsePromise.complete(); - return; - } - if (response.isComplete) { + } else if (response.isComplete) { responsePromise.complete(); - return; } - - const edits = response.response.value.map(part => { - if (part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)) { - return part.edits; - } else { - return []; - } - }).flat(); - - // const edits = response.edits.get(this._session!.textModelN.uri) ?? []; - const newEdits = edits.slice(lastLength); - // console.log('NEW edits', newEdits, edits); - if (newEdits.length === 0) { - return; // NO change - } - lastLength = edits.length; - progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); - progressiveEditsClock.reset(); - - progressiveEditsQueue.queue(async () => { - - const startThen = this._session!.wholeRange.value.getStartPosition(); - - // making changes goes into a queue because otherwise the async-progress time will - // influence the time it takes to receive the changes and progressive typing will - // become infinitely fast - for (const edits of newEdits) { - await this._makeChanges(edits, { - duration: progressiveEditsAvgDuration.value, - token: progressiveEditsCts.token - }, isFirstChange); - - isFirstChange = false; - } - - // reshow the widget if the start position changed or shows at the wrong position - const startNow = this._session!.wholeRange.value.getStartPosition(); - if (!startNow.equals(startThen) || !this._zone.value.position?.equals(startNow)) { - this._showWidget(false, startNow.delta(-1)); - } - }); - })); + }; + store.add(response.onDidChange(handleResponse)); + handleResponse(); // (1) we must wait for the request to finish // (2) we must wait for all edits that came in via progress to complete await responsePromise.p; await progressiveEditsQueue.whenIdle(); + + if (response.isCanceled) { + // + await this._session.undoChangesUntil(response.requestId); + } + store.dispose(); - // todo@jrieken we can likely remove 'trackEdit' const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced'); this._session.wholeRange.fixup(diff?.changes ?? []); + await this._session.hunkData.recompute(editState, diff); - await this._session.hunkData.recompute(); - - this._zone.value.widget.updateToolbar(true); - this._zone.value.widget.updateProgress(false); + this._ctxRequestInProgress.set(false); return next; } @@ -734,43 +673,39 @@ export class InlineChatController implements IEditorContribution { const { response } = this._session.lastExchange!; - let responseTypes: InlineChatResponseTypes | undefined; - for (const request of this._session.chatModel.getRequests()) { - if (!request.response) { - continue; - } - const thisType = asInlineChatResponseType(request.response.response); - if (responseTypes === undefined) { - responseTypes = thisType; - } else if (responseTypes !== thisType) { - responseTypes = InlineChatResponseTypes.Mixed; - break; - } - } - this._ctxResponseTypes.set(responseTypes); - this._ctxDidEdit.set(this._session.hasChangedText); let newPosition: Position | undefined; if (response instanceof EmptyResponse) { // show status message const status = localize('empty', "No results, please refine your input and try again"); - this._zone.value.widget.updateStatus(status, { classes: ['warn'] }); + this._ui.value.zone.widget.updateStatus(status, { classes: ['warn'] }); return State.WAIT_FOR_INPUT; } else if (response instanceof ErrorResponse) { // show error if (!response.isCancellation) { - this._zone.value.widget.updateStatus(response.message, { classes: ['error'] }); + this._ui.value.zone.widget.updateStatus(response.message, { classes: ['error'] }); this._strategy?.cancel(); } } else if (response instanceof ReplyResponse) { // real response -> complex... - this._zone.value.widget.updateStatus(''); - this._zone.value.widget.updateToolbar(true); + this._ui.value.zone.widget.updateStatus(''); - newPosition = await this._strategy.renderChanges(response); + const position = await this._strategy.renderChanges(); + if (position) { + // if the selection doesn't start far off we keep the widget at its current position + // because it makes reading this nicer + const selection = this._editor.getSelection(); + if (selection?.containsPosition(position)) { + if (position.lineNumber - selection.startLineNumber > 8) { + newPosition = position; + } + } else { + newPosition = position; + } + } } this._showWidget(false, newPosition); @@ -814,7 +749,7 @@ export class InlineChatController implements IEditorContribution { this._sessionStore.clear(); // only stash sessions that were not unstashed, not "empty", and not interacted with - const shouldStash = !this._session.isUnstashed && !!this._session.lastExchange && this._session.hunkData.size === this._session.hunkData.pending; + const shouldStash = !this._session.isUnstashed && this._session.chatModel.hasRequests && this._session.hunkData.size === this._session.hunkData.pending; let undoCancelEdits: IValidEditOperation[] = []; try { undoCancelEdits = this._strategy.cancel(); @@ -843,68 +778,54 @@ export class InlineChatController implements IEditorContribution { private _showWidget(initialRender: boolean = false, position?: Position) { assertType(this._editor.hasModel()); + this._ctxVisible.set(true); let widgetPosition: Position; if (position) { // explicit position wins widgetPosition = position; - } else if (this._zone.rawValue?.position) { + } else if (this._ui.rawValue?.zone?.position) { // already showing - special case of line 1 - if (this._zone.rawValue.position.lineNumber === 1) { - widgetPosition = this._zone.rawValue.position.delta(-1); + if (this._ui.rawValue?.zone.position.lineNumber === 1) { + widgetPosition = this._ui.rawValue?.zone.position.delta(-1); } else { - widgetPosition = this._zone.rawValue.position; + widgetPosition = this._ui.rawValue?.zone.position; } } else { // default to ABOVE the selection widgetPosition = this._editor.getSelection().getStartPosition().delta(-1); } - if (this._session && !position && (this._session.hasChangedText || this._session.lastExchange)) { + if (this._session && !position && (this._session.hasChangedText || this._session.chatModel.hasRequests)) { widgetPosition = this._session.wholeRange.value.getStartPosition().delta(-1); } - if (this._zone.rawValue?.position) { - this._zone.value.updatePositionAndHeight(widgetPosition); + if (this._ui.rawValue?.zone?.position) { + this._ui.value.zone.updatePositionAndHeight(widgetPosition); } else if (initialRender) { const selection = this._editor.getSelection(); widgetPosition = selection.getStartPosition(); - // TODO@jrieken we are not ready for this - // widgetPosition = selection.getEndPosition(); - // if (Range.spansMultipleLines(selection) && widgetPosition.column === 1) { - // // selection ends on "nothing" -> move up to match the - // // rendered/visible part of the selection - // widgetPosition = this._editor.getModel().validatePosition(widgetPosition.delta(-1, Number.MAX_SAFE_INTEGER)); - // } - this._input.value.show(widgetPosition); + this._ui.value.content.show(widgetPosition); } else { - this._input.value.hide(); - this._zone.value.show(widgetPosition); + this._ui.value.content.hide(); + this._ui.value.zone.show(widgetPosition); if (this._session) { - this._zone.value.widget.setChatModel(this._session.chatModel); + this._ui.value.zone.widget.setChatModel(this._session.chatModel); } } - if (this._session && this._zone.rawValue) { - this._zone.rawValue.updateBackgroundColor(widgetPosition, this._session.wholeRange.value); - } - - this._ctxVisible.set(true); return widgetPosition; } private _resetWidget() { this._sessionStore.clear(); this._ctxVisible.reset(); - this._ctxDidEdit.reset(); this._ctxUserDidEdit.reset(); - this._ctxLastFeedbackKind.reset(); - this._ctxSupportIssueReporting.reset(); - this._input.rawValue?.hide(); - this._zone.rawValue?.hide(); + this._ui.rawValue?.content.hide(); + this._ui.rawValue?.zone?.hide(); // Return focus to the editor only if the current focus is within the editor widget if (this._editor.hasWidgetFocus()) { @@ -912,6 +833,31 @@ export class InlineChatController implements IEditorContribution { } } + private _updateCtxResponseType(): void { + + if (!this._session) { + this._ctxResponseType.set(InlineChatResponseType.None); + return; + } + + const hasLocalEdit = (response: IResponse): boolean => { + return response.value.some(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)); + }; + + let responseType = InlineChatResponseType.None; + for (const request of this._session.chatModel.getRequests()) { + if (!request.response) { + continue; + } + responseType = InlineChatResponseType.Messages; + if (hasLocalEdit(request.response.response)) { + responseType = InlineChatResponseType.MessagesAndEdits; + break; // no need to check further + } + } + this._ctxResponseType.set(responseType); + } + private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined, undoStopBefore: boolean) { assertType(this._session); assertType(this._strategy); @@ -933,31 +879,28 @@ export class InlineChatController implements IEditorContribution { }; this._inlineChatSavingService.markChanged(this._session); - this._session.wholeRange.trackEdits(editOperations); if (opts) { await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore); } else { await this._strategy.makeChanges(editOperations, editsObserver, undoStopBefore); } - this._ctxDidEdit.set(this._session.hasChangedText); - } private _forcedPlaceholder: string | undefined = undefined; private _updatePlaceholder(): void { - this._zone.value.widget.placeholder = this._getPlaceholderText(); + this._ui.value.zone.widget.placeholder = this._getPlaceholderText(); } private _getPlaceholderText(): string { - return this._forcedPlaceholder ?? this._session?.session.placeholder ?? ''; + return this._forcedPlaceholder ?? this._session?.agent.description ?? ''; } // ---- controller API showSaveHint(): void { const status = localize('savehint', "Accept or discard changes to continue saving"); - this._zone.value.widget.updateStatus(status, { classes: ['warn'] }); + this._ui.value.zone.widget.updateStatus(status, { classes: ['warn'] }); } acceptInput() { @@ -966,12 +909,12 @@ export class InlineChatController implements IEditorContribution { updateInput(text: string, selectAll = true): void { - this._input.value.chatWidget.setInput(text); - this._zone.value.widget.chatWidget.setInput(text); + this._ui.value.content.chatWidget.setInput(text); + this._ui.value.zone.widget.chatWidget.setInput(text); if (selectAll) { const newSelection = new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1); - this._input.value.chatWidget.inputEditor.setSelection(newSelection); - this._zone.value.widget.chatWidget.inputEditor.setSelection(newSelection); + this._ui.value.content.chatWidget.inputEditor.setSelection(newSelection); + this._ui.value.zone.widget.chatWidget.inputEditor.setSelection(newSelection); } } @@ -980,9 +923,9 @@ export class InlineChatController implements IEditorContribution { } arrowOut(up: boolean): void { - if (this._zone.value.position && this._editor.hasModel()) { + if (this._ui.value.zone.position && this._editor.hasModel()) { const { column } = this._editor.getPosition(); - const { lineNumber } = this._zone.value.position; + const { lineNumber } = this._ui.value.zone.position; const newLine = up ? lineNumber : lineNumber + 1; this._editor.setPosition({ lineNumber: newLine, column }); this._editor.focus(); @@ -990,11 +933,11 @@ export class InlineChatController implements IEditorContribution { } focus(): void { - this._zone.value.widget.focus(); + this._ui.value.zone.widget.focus(); } hasFocus(): boolean { - return this._zone.value.widget.hasFocus(); + return this._ui.value.zone.widget.hasFocus(); } moveHunk(next: boolean) { @@ -1007,15 +950,33 @@ export class InlineChatController implements IEditorContribution { return; } - // TODO@jrieken REMOVE this as soon as we can mark responses as accepted - // and as soon as hunks support request-linking - const textEditsResponseCount = this._session.chatModel.getRequests().filter(request => request.response?.response.value.some(part => part.kind === 'textEditGroup')).length; - if (textEditsResponseCount > 1) { - return; + let someApplied = false; + let lastEdit: IChatTextEditGroup | undefined; + + const uri = this._editor.getModel()?.uri; + const requests = this._session.chatModel.getRequests(); + for (const request of requests) { + if (!request.response) { + continue; + } + for (const part of request.response.response.value) { + if (part.kind === 'textEditGroup' && isEqual(part.uri, uri)) { + // fully or partially applied edits + someApplied = someApplied || Boolean(part.state?.applied); + lastEdit = part; + } + } + } + + const doEdits = this._strategy.cancel(); + + if (someApplied) { + assertType(lastEdit); + lastEdit.edits = [doEdits]; } - this._strategy.cancel(); await this._instaService.invokeFunction(moveToPanelChat, this._session?.chatModel); + this.cancelSession(); } @@ -1023,19 +984,14 @@ export class InlineChatController implements IEditorContribution { this._strategy?.toggleDiff?.(); } - createSnapshot(): void { - if (this._session && !this._session.textModel0.equalsTextBuffer(this._session.textModelN.getTextBuffer())) { - this._session.createSnapshot(); - } - } - acceptSession(): void { - if (this._session?.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) { - const response = this._session?.lastExchange?.response.chatResponse; + const response = this._session?.chatModel.getRequests().at(-1)?.response; + if (response) { this._chatService.notifyUserAction({ - sessionId: this._session.chatModel.sessionId, + sessionId: response.session.sessionId, requestId: response.requestId, agentId: response.agent?.id, + command: response.slashCommand?.name, result: response.result, action: { kind: 'inlineChat', @@ -1055,30 +1011,22 @@ export class InlineChatController implements IEditorContribution { } async cancelSession() { - - let result: string | undefined; - if (this._session) { - - const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000, computeMoves: false }, 'advanced'); - result = this._session.asChangedText(diff?.changes ?? []); - - if (this._session.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) { - const response = this._session?.lastExchange?.response.chatResponse; - this._chatService.notifyUserAction({ - sessionId: this._session.chatModel.sessionId, - requestId: response.requestId, - agentId: response.agent?.id, - result: response.result, - action: { - kind: 'inlineChat', - action: 'discarded' - } - }); - } + const response = this._session?.chatModel.getRequests().at(-1)?.response; + if (response) { + this._chatService.notifyUserAction({ + sessionId: response.session.sessionId, + requestId: response.requestId, + agentId: response.agent?.id, + command: response.slashCommand?.name, + result: response.result, + action: { + kind: 'inlineChat', + action: 'discarded' + } + }); } this._messages.fire(Message.CANCEL_SESSION); - return result; } finishExistingSession(): void { @@ -1120,25 +1068,3 @@ async function moveToPanelChat(accessor: ServicesAccessor, model: ChatModel | un widget.focusLastMessage(); } } - -function asInlineChatResponseType(response: IResponse): InlineChatResponseTypes { - let result: InlineChatResponseTypes | undefined; - for (const item of response.value) { - let thisType: InlineChatResponseTypes; - switch (item.kind) { - case 'textEditGroup': - thisType = InlineChatResponseTypes.OnlyEdits; - break; - case 'markdownContent': - default: - thisType = InlineChatResponseTypes.OnlyMessages; - break; - } - if (result === undefined) { - result = thisType; - } else if (result !== thisType) { - return InlineChatResponseTypes.Mixed; - } - } - return result ?? InlineChatResponseTypes.Empty; -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget.ts deleted file mode 100644 index eca335cb375..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget.ts +++ /dev/null @@ -1,256 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Dimension, h } from 'vs/base/browser/dom'; -import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Range } from 'vs/editor/common/core/range'; -import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; -import * as editorColorRegistry from 'vs/editor/common/core/editorColorRegistry'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { INLINE_CHAT_ID, inlineChatRegionHighlight } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { Position } from 'vs/editor/common/core/position'; -import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { ResourceLabel } from 'vs/workbench/browser/labels'; -import { FileKind } from 'vs/platform/files/common/files'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IAction, toAction } from 'vs/base/common/actions'; -import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; -import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { Codicon } from 'vs/base/common/codicons'; -import { TAB_ACTIVE_MODIFIED_BORDER } from 'vs/workbench/common/theme'; -import { localize } from 'vs/nls'; -import { Event } from 'vs/base/common/event'; - -export class InlineChatFileCreatePreviewWidget extends ZoneWidget { - - private static TitleHeight = 35; - - private readonly _elements = h('div.inline-chat-newfile-widget@domNode', [ - h('div.title@title', [ - h('span.name.show-file-icons@name'), - h('span.detail@detail'), - ]), - h('div.editor@editor'), - ]); - - private readonly _name: ResourceLabel; - private readonly _previewEditor: ICodeEditor; - private readonly _previewStore = new MutableDisposable(); - private readonly _buttonBar: ButtonBarWidget; - private _dim: Dimension | undefined; - - constructor( - parentEditor: ICodeEditor, - @IInstantiationService instaService: IInstantiationService, - @IThemeService themeService: IThemeService, - @ITextModelService private readonly _textModelResolverService: ITextModelService, - @IEditorService private readonly _editorService: IEditorService, - ) { - super(parentEditor, { - showArrow: false, - showFrame: true, - frameColor: colorRegistry.asCssVariable(TAB_ACTIVE_MODIFIED_BORDER), - frameWidth: 1, - isResizeable: true, - isAccessible: true, - showInHiddenAreas: true, - ordinal: 10000 + 2 - }); - super.create(); - - this._name = instaService.createInstance(ResourceLabel, this._elements.name, { supportIcons: true }); - this._elements.detail.appendChild(renderIcon(Codicon.circleFilled)); - - const contributions = EditorExtensionsRegistry - .getEditorContributions() - .filter(c => c.id !== INLINE_CHAT_ID); - - this._previewEditor = instaService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, { - scrollBeyondLastLine: false, - stickyScroll: { enabled: false }, - minimap: { enabled: false }, - scrollbar: { alwaysConsumeMouseWheel: false, useShadows: true, ignoreHorizontalScrollbarInContentHeight: true, }, - }, { isSimpleWidget: true, contributions }, parentEditor); - - const doStyle = () => { - const theme = themeService.getColorTheme(); - const overrides: [target: string, source: string][] = [ - [colorRegistry.editorBackground, inlineChatRegionHighlight], - [editorColorRegistry.editorGutter, inlineChatRegionHighlight], - ]; - - for (const [target, source] of overrides) { - const value = theme.getColor(source); - if (value) { - this._elements.domNode.style.setProperty(colorRegistry.asCssVariableName(target), String(value)); - } - } - }; - doStyle(); - this._disposables.add(themeService.onDidColorThemeChange(doStyle)); - - this._buttonBar = instaService.createInstance(ButtonBarWidget); - this._elements.title.appendChild(this._buttonBar.domNode); - } - - override dispose(): void { - this._name.dispose(); - this._buttonBar.dispose(); - this._previewEditor.dispose(); - this._previewStore.dispose(); - super.dispose(); - } - - protected override _fillContainer(container: HTMLElement): void { - container.appendChild(this._elements.domNode); - } - - override show(): void { - throw new Error('Use showFileCreation'); - } - - async showCreation(where: Position, untitledTextModel: IUntitledTextEditorModel): Promise { - - const store = new DisposableStore(); - this._previewStore.value = store; - - this._name.element.setFile(untitledTextModel.resource, { - fileKind: FileKind.FILE, - fileDecorations: { badges: true, colors: true } - }); - - const actionSave = toAction({ - id: '1', - label: localize('save', "Create"), - run: () => untitledTextModel.save({ reason: SaveReason.EXPLICIT }) - }); - const actionSaveAs = toAction({ - id: '2', - label: localize('saveAs', "Create As"), - run: async () => { - const ids = this._editorService.findEditors(untitledTextModel.resource, { supportSideBySide: SideBySideEditor.ANY }); - await this._editorService.save(ids.slice(), { saveAs: true, reason: SaveReason.EXPLICIT }); - } - }); - - this._buttonBar.update([ - [actionSave, actionSaveAs], - [(toAction({ id: '3', label: localize('discard', "Discard"), run: () => untitledTextModel.revert() }))] - ]); - - store.add(Event.any( - untitledTextModel.onDidRevert, - untitledTextModel.onDidSave, - untitledTextModel.onDidChangeDirty, - untitledTextModel.onWillDispose - )(() => this.hide())); - - await untitledTextModel.resolve(); - - const ref = await this._textModelResolverService.createModelReference(untitledTextModel.resource); - store.add(ref); - - const model = ref.object.textEditorModel; - this._previewEditor.setModel(model); - - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - - this._elements.title.style.height = `${InlineChatFileCreatePreviewWidget.TitleHeight}px`; - const titleHightInLines = InlineChatFileCreatePreviewWidget.TitleHeight / lineHeight; - - const maxLines = Math.max(4, Math.floor((this.editor.getLayoutInfo().height / lineHeight) * .33)); - const lines = Math.min(maxLines, model.getLineCount()); - - super.show(where, titleHightInLines + lines); - } - - override hide(): void { - this._previewStore.clear(); - super.hide(); - } - - // --- layout - - protected override revealRange(range: Range, isLastLine: boolean): void { - // ignore - } - - protected override _onWidth(widthInPixel: number): void { - if (this._dim) { - this._doLayout(this._dim.height, widthInPixel); - } - } - - protected override _doLayout(heightInPixel: number, widthInPixel: number): void { - - const { lineNumbersLeft } = this.editor.getLayoutInfo(); - this._elements.title.style.marginLeft = `${lineNumbersLeft}px`; - - const newDim = new Dimension(widthInPixel, heightInPixel); - if (!Dimension.equals(this._dim, newDim)) { - this._dim = newDim; - this._previewEditor.layout(this._dim.with(undefined, this._dim.height - InlineChatFileCreatePreviewWidget.TitleHeight)); - } - } -} - - -class ButtonBarWidget { - - private readonly _domNode = h('div.buttonbar-widget'); - private readonly _buttonBar: ButtonBar; - private readonly _store = new DisposableStore(); - - constructor( - @IContextMenuService private _contextMenuService: IContextMenuService, - ) { - this._buttonBar = new ButtonBar(this.domNode); - - } - - update(allActions: IAction[][]): void { - this._buttonBar.clear(); - let secondary = false; - for (const actions of allActions) { - let btn: IButton; - const [first, ...rest] = actions; - if (!first) { - continue; - } else if (rest.length === 0) { - // single action - btn = this._buttonBar.addButton({ ...defaultButtonStyles, secondary }); - } else { - btn = this._buttonBar.addButtonWithDropdown({ - ...defaultButtonStyles, - addPrimaryActionToDropdown: false, - actions: rest, - contextMenuProvider: this._contextMenuService - }); - } - btn.label = first.label; - this._store.add(btn.onDidClick(() => first.run())); - secondary = true; - } - } - - dispose(): void { - this._buttonBar.dispose(); - this._store.dispose(); - } - - get domNode() { - return this._domNode.root; - } -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 2bebeaede34..4d18fe88fce 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -5,17 +5,15 @@ import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; -import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; -import { IWorkspaceTextEdit, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; +import { TextEdit } from 'vs/editor/common/languages'; import { IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { EditMode, IInlineChatSession, IInlineChatBulkEditResponse, IInlineChatEditResponse, InlineChatResponseType, CTX_INLINE_CHAT_HAS_STASHED_SESSION } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { EditMode, CTX_INLINE_CHAT_HAS_STASHED_SESSION } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isCancellationError } from 'vs/base/common/errors'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { DetailedLineRangeMapping, LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -32,9 +30,10 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ILogService } from 'vs/platform/log/common/log'; -import { ChatModel, IChatRequestModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroupState } from 'vs/workbench/contrib/chat/common/chatModel'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IChatAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IDocumentDiff } from 'vs/editor/common/diff/documentDiffProvider'; export type TelemetryData = { @@ -90,15 +89,6 @@ export class SessionWholeRange { } } - trackEdits(edits: ISingleEditOperation[]): void { - const newDeco: IModelDeltaDecoration[] = []; - for (const edit of edits) { - newDeco.push({ range: edit.range, options: SessionWholeRange._options }); - } - this._decorationIds.push(...this._textModel.deltaDecorations([], newDeco)); - this._onDidChange.fire(this); - } - fixup(changes: readonly DetailedLineRangeMapping[]): void { const newDeco: IModelDeltaDecoration[] = []; @@ -140,12 +130,11 @@ export class Session { private _lastInput: SessionPrompt | undefined; private _isUnstashed: boolean = false; - private readonly _exchange: SessionExchange[] = []; + private readonly _exchanges: SessionExchange[] = []; private readonly _startTime = new Date(); private readonly _teldata: TelemetryData; readonly textModelNAltVersion: number; - private _textModelNSnapshotAltVersion: number | undefined; constructor( readonly editMode: EditMode, @@ -158,11 +147,10 @@ export class Session { */ readonly textModel0: ITextModel, /** - * The document into which AI edits went, when live this is `targetUri` otherwise it is a temporary document + * The model of the editor */ readonly textModelN: ITextModel, readonly agent: IChatAgent, - readonly session: IInlineChatSession, readonly wholeRange: SessionWholeRange, readonly hunkData: HunkData, readonly chatModel: ChatModel, @@ -201,23 +189,37 @@ export class Session { this._isUnstashed = true; } - get textModelNSnapshotAltVersion(): number | undefined { - return this._textModelNSnapshotAltVersion; - } - - createSnapshot(): void { - this._textModelNSnapshotAltVersion = this.textModelN.getAlternativeVersionId(); - } - addExchange(exchange: SessionExchange): void { this._isUnstashed = false; - const newLen = this._exchange.push(exchange); + const newLen = this._exchanges.push(exchange); this._teldata.rounds += `${newLen}|`; // this._teldata.responseTypes += `${exchange.response instanceof ReplyResponse ? exchange.response.responseType : InlineChatResponseTypes.Empty}|`; } get lastExchange(): SessionExchange | undefined { - return this._exchange[this._exchange.length - 1]; + return this._exchanges[this._exchanges.length - 1]; + } + + async undoChangesUntil(requestId: string): Promise { + const idx = this._exchanges.findIndex(candidate => candidate.prompt.request.id === requestId); + if (idx < 0) { + return false; + } + // undo till this point + this.hunkData.ignoreTextModelNChanges = true; + try { + const targetAltVersion = this._exchanges[idx].prompt.modelAltVersionId; + while (targetAltVersion < this.textModelN.getAlternativeVersionId() && this.textModelN.canUndo()) { + await this.textModelN.undo(); + } + } finally { + this.hunkData.ignoreTextModelNChanges = false; + } + // TODO@jrieken cannot do this yet because some parts still rely on + // exchanges being around... + // // remove this and following exchanges + // this._exchanges.length = idx; + return true; } get hasChangedText(): boolean { @@ -263,14 +265,14 @@ export class Session { asRecording(): Recording { const result: Recording = { - session: this.session, + session: this.chatModel.sessionId, when: this._startTime, exchanges: [] }; - for (const exchange of this._exchange) { + for (const exchange of this._exchanges) { const response = exchange.response; if (response instanceof ReplyResponse) { - result.exchanges.push({ prompt: exchange.prompt.value, res: response.raw }); + result.exchanges.push({ prompt: exchange.prompt.value, res: response.chatResponse }); } } return result; @@ -283,7 +285,8 @@ export class SessionPrompt { readonly value: string; constructor( - readonly request: IChatRequestModel + readonly request: IChatRequestModel, + readonly modelAltVersionId: number, ) { this.value = request.message.text; } @@ -316,57 +319,31 @@ export class ErrorResponse { export class ReplyResponse { - readonly allLocalEdits: TextEdit[][] = []; readonly untitledTextModel: IUntitledTextEditorModel | undefined; - readonly workspaceEdit: WorkspaceEdit | undefined; - constructor( - readonly raw: IInlineChatBulkEditResponse | IInlineChatEditResponse, - readonly mdContent: IMarkdownString, localUri: URI, - readonly modelAltVersionId: number, - progressEdits: TextEdit[][], - readonly requestId: string, - readonly chatResponse: IChatResponseModel | undefined, + readonly chatRequest: IChatRequestModel, + readonly chatResponse: IChatResponseModel, @ITextFileService private readonly _textFileService: ITextFileService, @ILanguageService private readonly _languageService: ILanguageService, ) { const editsMap = new ResourceMap(); - editsMap.set(localUri, [...progressEdits]); - - if (raw.type === InlineChatResponseType.EditorEdit) { - // - editsMap.get(localUri)!.push(raw.edits); - - } else if (raw.type === InlineChatResponseType.BulkEdit) { - // - const edits = ResourceEdit.convert(raw.edits); - - for (const edit of edits) { - if (edit instanceof ResourceFileEdit) { - if (edit.newResource && !edit.oldResource) { - editsMap.set(edit.newResource, []); - if (edit.options.contents) { - console.warn('CONTENT not supported'); - } - } - } else if (edit instanceof ResourceTextEdit) { - // - const array = editsMap.get(edit.resource); + for (const item of chatResponse.response.value) { + if (item.kind === 'textEditGroup') { + const array = editsMap.get(item.uri); + for (const group of item.edits) { if (array) { - array.push([edit.textEdit]); + array.push(group); } else { - editsMap.set(edit.resource, [[edit.textEdit]]); + editsMap.set(item.uri, [group]); } } } } - let needsWorkspaceEdit = false; - for (const [uri, edits] of editsMap) { const flatEdits = edits.flat(); @@ -376,8 +353,6 @@ export class ReplyResponse { } const isLocalUri = isEqual(uri, localUri); - needsWorkspaceEdit = needsWorkspaceEdit || (uri.scheme !== Schemas.untitled && !isLocalUri); - if (uri.scheme === Schemas.untitled && !isLocalUri && !this.untitledTextModel) { //TODO@jrieken the first untitled model WINS const langSelection = this._languageService.createByFilepathOrFirstLine(uri, undefined); const untitledTextModel = this._textFileService.untitled.create({ @@ -388,18 +363,6 @@ export class ReplyResponse { untitledTextModel.resolve(); } } - - this.allLocalEdits = editsMap.get(localUri) ?? []; - - if (needsWorkspaceEdit) { - const workspaceEdits: IWorkspaceTextEdit[] = []; - for (const [uri, edits] of editsMap) { - for (const edit of edits.flat()) { - workspaceEdits.push({ resource: uri, textEdit: edit, versionId: undefined }); - } - } - this.workspaceEdit = { edits: workspaceEdits }; - } } } @@ -471,7 +434,7 @@ export class HunkData { private static readonly _HUNK_THRESHOLD = 8; private readonly _store = new DisposableStore(); - private readonly _data = new Map(); + private readonly _data = new Map(); private _ignoreChanges: boolean = false; constructor( @@ -602,9 +565,9 @@ export class HunkData { this._textModel0.pushEditOperations(null, edits, () => null); } - async recompute() { + async recompute(editState: IChatTextEditGroupState, diff?: IDocumentDiff | null) { - const diff = await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced'); + diff ??= await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced'); if (!diff || diff.changes.length === 0) { // return new HunkData([], session); @@ -656,6 +619,7 @@ export class HunkData { } this._data.set(hunk, { + editState, textModelNDecorations, textModel0Decorations, state: HunkState.Pending @@ -689,7 +653,7 @@ export class HunkData { discardAll() { const edits: ISingleEditOperation[][] = []; for (const item of this.getInfo()) { - if (item.getState() !== HunkState.Rejected) { + if (item.getState() === HunkState.Pending) { edits.push(this._discardEdits(item)); } } @@ -746,6 +710,7 @@ export class HunkData { } this._textModel0.pushEditOperations(null, edits, () => null); data.state = HunkState.Accepted; + data.editState.applied += 1; } } }; @@ -764,6 +729,13 @@ class RawHunk { ) { } } +type RawHunkData = { + textModelNDecorations: string[]; + textModel0Decorations: string[]; + state: HunkState; + editState: IChatTextEditGroupState; +}; + export const enum HunkState { Pending = 0, Accepted = 1, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 638f0858903..6ab4ea5dbec 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; -import { EditMode, IInlineChatSession, IInlineChatResponse } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { EditMode } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IRange } from 'vs/editor/common/core/range'; import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -12,12 +12,13 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Session, StashedSession } from './inlineChatSession'; import { IValidEditOperation } from 'vs/editor/common/model'; +import { IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; export type Recording = { when: Date; - session: IInlineChatSession; - exchanges: { prompt: string; res: IInlineChatResponse }[]; + session: string; + exchanges: { prompt: string; res: IChatResponseModel }[]; }; export interface ISessionKeyComputer { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index e76cd173882..44aecd6d917 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -5,13 +5,12 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { MarkdownString } from 'vs/base/common/htmlContent'; import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { IValidEditOperation } from 'vs/editor/common/model'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; @@ -23,13 +22,11 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { CTX_INLINE_CHAT_HAS_PROVIDER, EditMode, IInlineChatBulkEditResponse, IInlineChatSession, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_HAS_AGENT, EditMode } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { EmptyResponse, ErrorResponse, HunkData, ReplyResponse, Session, SessionExchange, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession'; import { IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer, Recording } from './inlineChatSessionService'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { ISelection } from 'vs/editor/common/core/selection'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -47,19 +44,6 @@ export class InlineChatError extends Error { } } -const _inlineChatContext = '_inlineChatContext'; -const _inlineChatDocument = '_inlineChatDocument'; - -class InlineChatContext { - - static readonly variableName = '_inlineChatContext'; - - constructor( - readonly uri: URI, - readonly selection: ISelection, - readonly wholeRange: IRange, - ) { } -} export class InlineChatSessionServiceImpl implements IInlineChatSessionService { @@ -93,37 +77,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { @IInstantiationService private readonly _instaService: IInstantiationService, @IEditorService private readonly _editorService: IEditorService, @IChatService private readonly _chatService: IChatService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, - @IChatVariablesService chatVariableService: IChatVariablesService, - ) { - - - // MARK: implicit variable for editor selection and (tracked) whole range - - this._store.add(chatVariableService.registerVariable( - { id: _inlineChatContext, name: _inlineChatContext, description: '', hidden: true }, - async (_message, _arg, model) => { - for (const [, data] of this._sessions) { - if (data.session.chatModel === model) { - return JSON.stringify(new InlineChatContext(data.session.textModelN.uri, data.editor.getSelection()!, data.session.wholeRange.trackedInitialRange)); - } - } - return undefined; - } - )); - this._store.add(chatVariableService.registerVariable( - { id: _inlineChatDocument, name: _inlineChatDocument, description: '', hidden: true }, - async (_message, _arg, model) => { - for (const [, data] of this._sessions) { - if (data.session.chatModel === model) { - return data.session.textModelN.uri; - } - } - return undefined; - } - )); - - } + @IChatAgentService private readonly _chatAgentService: IChatAgentService + ) { } dispose() { this._store.dispose(); @@ -146,13 +101,6 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const textModel = editor.getModel(); const selection = editor.getSelection(); - const rawSession: IInlineChatSession = { - id: Math.random(), - wholeRange: new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn), - placeholder: agent.description, - slashCommands: agent.slashCommands - }; - const store = new DisposableStore(); this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${agent.extensionId}`); @@ -173,8 +121,6 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { return; } - const modelAltVersionIdNow = textModel.getAlternativeVersionId(); - const { response } = e.request; lastResponseListener.value = response.onDidChange(() => { @@ -198,39 +144,11 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { // epmty response inlineResponse = new EmptyResponse(); } else { - // replay response - const markdownContent = new MarkdownString(); - const raw: IInlineChatBulkEditResponse = { - id: Math.random(), - type: InlineChatResponseType.BulkEdit, - message: markdownContent, - edits: { edits: [] }, - }; - for (const item of response.response.value) { - if (item.kind === 'markdownContent') { - markdownContent.value += item.content.value; - } else if (item.kind === 'textEditGroup') { - for (const group of item.edits) { - for (const edit of group) { - raw.edits.edits.push({ - resource: item.uri, - textEdit: edit, - versionId: undefined - }); - } - } - } - } - inlineResponse = this._instaService.createInstance( ReplyResponse, - raw, - markdownContent, session.textModelN.uri, - modelAltVersionIdNow, - [], - e.request.id, - e.request.response + e.request, + response ); } @@ -276,7 +194,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { let wholeRange = options.wholeRange; if (!wholeRange) { - wholeRange = rawSession.wholeRange ? Range.lift(rawSession.wholeRange) : editor.getSelection(); + wholeRange = new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn); } if (token.isCancellationRequested) { @@ -290,7 +208,6 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { textModel0, textModelN, agent, - rawSession, store.add(new SessionWholeRange(textModelN, wholeRange)), store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)), chatModel @@ -422,18 +339,21 @@ export class InlineChatEnabler { private readonly _ctxHasProvider: IContextKey; + private readonly _store = new DisposableStore(); + constructor( @IContextKeyService contextKeyService: IContextKeyService, @IChatAgentService chatAgentService: IChatAgentService ) { - this._ctxHasProvider = CTX_INLINE_CHAT_HAS_PROVIDER.bindTo(contextKeyService); - chatAgentService.onDidChangeAgents(() => { + this._ctxHasProvider = CTX_INLINE_CHAT_HAS_AGENT.bindTo(contextKeyService); + this._store.add(chatAgentService.onDidChangeAgents(() => { const hasEditorAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); this._ctxHasProvider.set(hasEditorAgent); - }); + })); } dispose() { this._ctxHasProvider.reset(); + this._store.dispose(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 458c0da69a6..2e2f692cc88 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -26,7 +26,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { Progress } from 'vs/platform/progress/common/progress'; import { SaveReason } from 'vs/workbench/common/editor'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -import { HunkInformation, ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { HunkState } from './inlineChatSession'; @@ -99,7 +99,7 @@ export abstract class EditModeStrategy { continue; } - await editor.apply(request.response, item); + await editor.apply(request.response, item, undefined); if (item.uri.scheme === Schemas.untitled) { const untitled = this._textFileService.untitled.get(item.uri); @@ -136,7 +136,7 @@ export abstract class EditModeStrategy { abstract makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise; - abstract renderChanges(response: ReplyResponse): Promise; + abstract renderChanges(): Promise; move?(next: boolean): void; @@ -190,7 +190,7 @@ export class PreviewStrategy extends EditModeStrategy { override async makeProgressiveChanges(): Promise { } - override async renderChanges(response: ReplyResponse): Promise { } + override async renderChanges(): Promise { } hasFocus(): boolean { return this._zone.widget.hasFocus(); @@ -364,7 +364,7 @@ export class LiveStrategy extends EditModeStrategy { private readonly _hunkDisplayData = new Map(); - override async renderChanges(response: ReplyResponse) { + override async renderChanges() { this._progressiveEditingDecorations.clear(); @@ -531,7 +531,6 @@ export class LiveStrategy extends EditModeStrategy { if (widgetData) { this._zone.updatePositionAndHeight(widgetData.position); - this._editor.revealPositionInCenterIfOutsideViewport(widgetData.position); const remainingHunks = this._session.hunkData.pending; this._updateSummaryMessage(remainingHunks, this._session.hunkData.size); @@ -578,7 +577,7 @@ export class LiveStrategy extends EditModeStrategy { message = localize('change.0', "Nothing changed."); } else if (remaining === 1) { message = needsReview - ? localize('review.1', "$(info) Accept or discard 1 change") + ? localize('review.1', "$(info) Accept or Discard change") : localize('change.1', "1 change"); } else { message = needsReview diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index efe64a7d919..e920c34947b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -5,7 +5,6 @@ import { Dimension, getActiveElement, getTotalHeight, h, reset, trackFocus } from 'vs/base/browser/dom'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; @@ -25,20 +24,20 @@ import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; -import { MenuId } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { asCssVariable, asCssVariableName, editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; +import { asCssVariable, asCssVariableName, editorBackground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { ChatModel, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, inlineChatBackground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, inlineChatBackground, InlineChatConfigKeys, inlineChatForeground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ChatWidget, IChatWidgetLocationOptions } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { chatRequestBackground } from 'vs/workbench/contrib/chat/common/chatColors'; import { Selection } from 'vs/editor/common/core/selection'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; @@ -47,7 +46,8 @@ import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { IChatListItemRendererOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatWidgetViewOptions } from 'vs/workbench/contrib/chat/browser/chat'; +import { TextOnlyMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export interface InlineChatWidgetViewState { @@ -57,30 +57,16 @@ export interface InlineChatWidgetViewState { } export interface IInlineChatWidgetConstructionOptions { - /** - * The telemetry source for all commands of this widget - */ - telemetrySource: string; - /** - * The menu that is inside the input editor, use for send, dictation - */ - inputMenuId: MenuId; - /** - * The menu that next to the input editor, use for close, config etc - */ - widgetMenuId: MenuId; + /** * The menu that rendered as button bar, use for accept, discard etc */ statusMenuId: MenuId | { menu: MenuId; options: IWorkbenchButtonBarOptions }; + /** - * The men that rendered in the lower right corner, use for feedback + * The options for the chat widget */ - feedbackMenuId?: MenuId; - - editorOverflowWidgetsDomNode?: HTMLElement; - - rendererOptions?: IChatListItemRendererOptions; + chatWidgetViewOptions?: IChatWidgetViewOptions; } export interface IInlineChatMessage { @@ -100,15 +86,12 @@ export class InlineChatWidget { 'div.inline-chat@root', [ h('div.chat-widget@chatWidget'), - h('div.progress@progress'), - h('div.followUps.hidden@followUps'), - h('div.previewDiff.hidden@previewDiff'), h('div.accessibleViewer@accessibleViewer'), h('div.status@status', [ h('div.label.info.hidden@infoLabel'), - h('div.actions.hidden@statusToolbar'), + h('div.actions.text-style.hidden@toolbar1'), + h('div.actions.button-style.hidden@toolbar2'), h('div.label.status.hidden@statusLabel'), - h('div.actions.hidden@feedbackToolbar'), ]), ] ); @@ -119,7 +102,6 @@ export class InlineChatWidget { private readonly _ctxInputEditorFocused: IContextKey; private readonly _ctxResponseFocused: IContextKey; - private readonly _progressBar: ProgressBar; private readonly _chatWidget: ChatWidget; protected readonly _onDidChangeHeight = this._store.add(new Emitter()); @@ -133,7 +115,7 @@ export class InlineChatWidget { readonly scopedContextKeyService: IContextKeyService; constructor( - location: ChatAgentLocation, + location: IChatWidgetLocationOptions, options: IInlineChatWidgetConstructionOptions, @IInstantiationService protected readonly _instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -145,12 +127,6 @@ export class InlineChatWidget { @IChatService private readonly _chatService: IChatService, @IHoverService private readonly _hoverService: IHoverService, ) { - // toolbars - this._progressBar = new ProgressBar(this._elements.progress); - this._store.add(this._progressBar); - - let allowRequests = false; - this.scopedContextKeyService = this._store.add(_contextKeyService.createScoped(this._elements.chatWidget)); const scopedInstaService = _instantiationService.createChild( new ServiceCollection([ @@ -166,29 +142,15 @@ export class InlineChatWidget { { resource: true }, { defaultElementHeight: 32, - renderStyle: 'compact', - renderInputOnTop: true, + renderStyle: 'minimal', + renderInputOnTop: false, renderFollowups: true, - supportsFileReferences: true, - editorOverflowWidgetsDomNode: options.editorOverflowWidgetsDomNode, - rendererOptions: options.rendererOptions, - menus: { - executeToolbar: options.inputMenuId, - inputSideToolbar: options.widgetMenuId, - telemetrySource: options.telemetrySource - }, - filter: item => { - if (isWelcomeVM(item)) { - return false; - } - if (isRequestVM(item)) { - return allowRequests; - } - return true; - }, + supportsFileReferences: false, + filter: item => !isWelcomeVM(item), + ...options.chatWidgetViewOptions }, { - listForeground: editorForeground, + listForeground: inlineChatForeground, listBackground: inlineChatBackground, inputEditorBackground: inputBackground, resultEditorBackground: editorBackground @@ -199,34 +161,6 @@ export class InlineChatWidget { this._chatWidget.setVisible(true); this._store.add(this._chatWidget); - const viewModelListener = this._store.add(new MutableDisposable()); - this._store.add(this._chatWidget.onDidChangeViewModel(() => { - const model = this._chatWidget.viewModel; - - if (!model) { - allowRequests = false; - viewModelListener.clear(); - return; - } - - const updateAllowRequestsFilter = () => { - let requestCount = 0; - for (const item of model.getItems()) { - if (isRequestVM(item)) { - if (++requestCount >= 2) { - break; - } - } - } - const newAllowRequest = requestCount >= 2; - if (newAllowRequest !== allowRequests) { - allowRequests = newAllowRequest; - this._chatWidget.refilter(); - } - }; - viewModelListener.value = model.onDidChange(updateAllowRequestsFilter); - })); - const viewModelStore = this._store.add(new DisposableStore()); this._store.add(this._chatWidget.onDidChangeViewModel(() => { viewModelStore.clear(); @@ -252,26 +186,35 @@ export class InlineChatWidget { this._store.add(this._chatWidget.inputEditor.onDidBlurEditorWidget(() => this._ctxInputEditorFocused.set(false))); const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; - const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; - const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, statusMenuId, statusMenuOptions); + // TEXT-ONLY bar + const statusToolbarMenu = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar1, statusMenuId, { + hiddenItemStrategy: HiddenItemStrategy.NoHide, + telemetrySource: options.chatWidgetViewOptions?.menus?.telemetrySource, + actionViewItemProvider: action => action instanceof MenuItemAction ? this._instantiationService.createInstance(TextOnlyMenuEntryActionViewItem, action, { conversational: true }) : undefined, + toolbarOptions: { primaryGroup: '0_main' }, + menuOptions: { renderShortTitle: true }, + label: true, + icon: false + }); + this._store.add(statusToolbarMenu.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); + this._store.add(statusToolbarMenu); + + // BUTTON bar + const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; + const statusButtonBar = scopedInstaService.createInstance(MenuWorkbenchButtonBar, this._elements.toolbar2, statusMenuId, { + toolbarOptions: { primaryGroup: '0_main' }, + telemetrySource: options.chatWidgetViewOptions?.menus?.telemetrySource, + menuOptions: { renderShortTitle: true }, + ...statusMenuOptions, + }); this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire())); this._store.add(statusButtonBar); + const toggleToolbar = () => this._elements.status.classList.toggle('text', this._configurationService.getValue(InlineChatConfigKeys.ExpTextButtons)); + this._store.add(this._configurationService.onDidChangeConfiguration(e => e.affectsConfiguration(InlineChatConfigKeys.ExpTextButtons) && toggleToolbar())); + toggleToolbar(); - const workbenchToolbarOptions = { - hiddenItemStrategy: HiddenItemStrategy.NoHide, - toolbarOptions: { - primaryGroup: () => true, - useSeparatorsInPrimaryActions: true - } - }; - - if (options.feedbackMenuId) { - const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); - this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); - this._store.add(feedbackToolbar); - } this._store.add(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { @@ -280,12 +223,11 @@ export class InlineChatWidget { })); this._elements.root.tabIndex = 0; - this._elements.followUps.tabIndex = 0; this._elements.statusLabel.tabIndex = 0; this._updateAriaLabel(); // this._elements.status - this._store.add(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { + this._store.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { return this._elements.statusLabel.dataset['title']; })); @@ -346,18 +288,15 @@ export class InlineChatWidget { protected _doLayout(dimension: Dimension): void { const extraHeight = this._getExtraHeight(); - const progressHeight = getTotalHeight(this._elements.progress); - const followUpsHeight = getTotalHeight(this._elements.followUps); const statusHeight = getTotalHeight(this._elements.status); // console.log('ZONE#Widget#layout', { height: dimension.height, extraHeight, progressHeight, followUpsHeight, statusHeight, LIST: dimension.height - progressHeight - followUpsHeight - statusHeight - extraHeight }); this._elements.root.style.height = `${dimension.height - extraHeight}px`; this._elements.root.style.width = `${dimension.width}px`; - this._elements.progress.style.width = `${dimension.width}px`; this._chatWidget.layout( - dimension.height - progressHeight - followUpsHeight - statusHeight - extraHeight, + dimension.height - statusHeight - extraHeight, dimension.width ); } @@ -367,13 +306,11 @@ export class InlineChatWidget { */ get contentHeight(): number { const data = { - followUpsHeight: getTotalHeight(this._elements.followUps), chatWidgetContentHeight: this._chatWidget.contentHeight, - progressHeight: getTotalHeight(this._elements.progress), statusHeight: getTotalHeight(this._elements.status), extraHeight: this._getExtraHeight() }; - const result = data.progressHeight + data.chatWidgetContentHeight + data.followUpsHeight + data.statusHeight + data.extraHeight; + const result = data.chatWidgetContentHeight + data.statusHeight + data.extraHeight; return result; } @@ -396,17 +333,7 @@ export class InlineChatWidget { } protected _getExtraHeight(): number { - return 12 /* padding */ + 2 /*border*/ + 12 /*shadow*/; - } - - updateProgress(show: boolean) { - if (show) { - this._progressBar.show(); - this._progressBar.infinite(); - } else { - this._progressBar.stop(); - this._progressBar.hide(); - } + return 4 /* padding */ + 2 /*border*/ + 4 /*shadow*/; } get value(): string { @@ -436,8 +363,9 @@ export class InlineChatWidget { } updateToolbar(show: boolean) { - this._elements.statusToolbar.classList.toggle('hidden', !show); - this._elements.feedbackToolbar.classList.toggle('hidden', !show); + this._elements.root.classList.toggle('toolbar', show); + this._elements.toolbar1.classList.toggle('hidden', !show); + this._elements.toolbar2.classList.toggle('hidden', !show); this._elements.status.classList.toggle('actions', show); this._elements.infoLabel.classList.toggle('hidden', show); this._onDidChangeHeight.fire(); @@ -448,12 +376,12 @@ export class InlineChatWidget { if (!viewModel) { return undefined; } - for (const item of viewModel.getItems()) { - if (isResponseVM(item)) { - return viewModel.codeBlockModelCollection.get(viewModel.sessionId, item, codeBlockIndex)?.model; - } + const items = viewModel.getItems().filter(i => isResponseVM(i)); + if (!items.length) { + return; } - return undefined; + const item = items[items.length - 1]; + return viewModel.codeBlockModelCollection.get(viewModel.sessionId, item, codeBlockIndex)?.model; } get responseContent(): string | undefined { @@ -461,12 +389,9 @@ export class InlineChatWidget { if (!isNonEmptyArray(requests)) { return undefined; } - return tail(requests)?.response?.response.asString(); + return tail(requests)?.response?.response.toString(); } - get usesDefaultChatModel(): boolean { - return this.getChatModel() === this._defaultChatModel; - } getChatModel(): IChatModel { return this._chatWidget.viewModel?.model ?? this._defaultChatModel; @@ -482,7 +407,7 @@ export class InlineChatWidget { */ addToHistory(input: string) { if (this._chatWidget.viewModel?.model === this._defaultChatModel) { - this._chatWidget.input.acceptInput(input); + this._chatWidget.input.acceptInput(true); } } @@ -570,10 +495,12 @@ export class InlineChatWidget { reset(this._elements.statusLabel); this._elements.statusLabel.classList.toggle('hidden', true); - this._elements.statusToolbar.classList.add('hidden'); - this._elements.feedbackToolbar.classList.add('hidden'); + this._elements.toolbar1.classList.add('hidden'); + this._elements.toolbar2.classList.add('hidden'); this.updateInfo(''); + this.chatWidget.setModel(this._defaultChatModel, {}); + this._elements.accessibleViewer.classList.toggle('hidden', true); this._onDidChangeHeight.fire(); } @@ -595,6 +522,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { private readonly _accessibleViewer = this._store.add(new MutableDisposable()); constructor( + location: IChatWidgetLocationOptions, private readonly _parentEditor: ICodeEditor, options: IInlineChatWidgetConstructionOptions, @IContextKeyService contextKeyService: IContextKeyService, @@ -607,18 +535,28 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { @IChatService chatService: IChatService, @IHoverService hoverService: IHoverService, ) { - super(ChatAgentLocation.Editor, { ...options, editorOverflowWidgetsDomNode: _parentEditor.getOverflowWidgetsDomNode() }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService, hoverService); + super(location, { ...options, chatWidgetViewOptions: { ...options.chatWidgetViewOptions, editorOverflowWidgetsDomNode: _parentEditor.getOverflowWidgetsDomNode() } }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService, hoverService); } // --- layout + override get contentHeight(): number { + let result = super.contentHeight; + + if (this._accessibleViewer.value) { + result += this._accessibleViewer.value.height + 8 /* padding */; + } + + return result; + } + protected override _doLayout(dimension: Dimension): void { let newHeight = dimension.height; if (this._accessibleViewer.value) { this._accessibleViewer.value.width = dimension.width - 12; - newHeight -= this._accessibleViewer.value.height; + newHeight -= this._accessibleViewer.value.height + 8; } super._doLayout(dimension.with(undefined, newHeight)); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 5dcca5cbb31..adc3b413558 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -9,19 +9,19 @@ import { assertType } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, EditMode, InlineChatConfigKeys, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, EditMode, InlineChatConfigKeys, MENU_INLINE_CHAT_EXECUTE, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { EditorBasedInlineChatWidget } from './inlineChatWidget'; -import { MenuId } from 'vs/platform/actions/common/actions'; import { isEqual } from 'vs/base/common/resources'; import { StableEditorBottomScrollState } from 'vs/editor/browser/stableEditorScroll'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; - +import { ILogService } from 'vs/platform/log/common/log'; +import { IChatWidgetLocationOptions } from 'vs/workbench/contrib/chat/browser/chatWidget'; export class InlineChatZoneWidget extends ZoneWidget { @@ -29,11 +29,12 @@ export class InlineChatZoneWidget extends ZoneWidget { private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; private _dimension?: Dimension; - private _indentationWidth: number | undefined; constructor( + location: IChatWidgetLocationOptions, editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, + @ILogService private _logService: ILogService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, ) { @@ -45,10 +46,7 @@ export class InlineChatZoneWidget extends ZoneWidget { this._ctxCursorPosition.reset(); })); - this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, this.editor, { - telemetrySource: 'interactiveEditorWidget-toolbar', - inputMenuId: MenuId.ChatExecute, - widgetMenuId: MENU_INLINE_CHAT_WIDGET, + this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, { statusMenuId: { menu: MENU_INLINE_CHAT_WIDGET_STATUS, options: { @@ -63,22 +61,41 @@ export class InlineChatZoneWidget extends ZoneWidget { } } }, - rendererOptions: { - renderTextEditsAsSummary: (uri) => { - // render edits as summary only when using Live mode and when - // dealing with the current file in the editor - return isEqual(uri, editor.getModel()?.uri) - && configurationService.getValue(InlineChatConfigKeys.Mode) === EditMode.Live; + chatWidgetViewOptions: { + menus: { + executeToolbar: MENU_INLINE_CHAT_EXECUTE, + telemetrySource: 'interactiveEditorWidget-toolbar', }, + rendererOptions: { + renderTextEditsAsSummary: (uri) => { + // render edits as summary only when using Live mode and when + // dealing with the current file in the editor + return isEqual(uri, editor.getModel()?.uri) + && configurationService.getValue(InlineChatConfigKeys.Mode) === EditMode.Live; + }, + } } }); + this._disposables.add(this.widget); + + let scrollState: StableEditorBottomScrollState | undefined; + this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => { + if (this.position) { + scrollState = StableEditorBottomScrollState.capture(this.editor); + } + })); this._disposables.add(this.widget.onDidChangeHeight(() => { if (this.position) { // only relayout when visible - this._relayout(this._computeHeightInLines()); + scrollState ??= StableEditorBottomScrollState.capture(this.editor); + const height = this._computeHeight(); + this._relayout(height.linesValue); + scrollState.restore(this.editor); + scrollState = undefined; + this._revealTopOfZoneWidget(this.position, height); } })); - this._disposables.add(this.widget); + this.create(); this._disposables.add(addDisposableListener(this.domNode, 'click', e => { @@ -109,25 +126,23 @@ export class InlineChatZoneWidget extends ZoneWidget { container.appendChild(this.widget.domNode); } - protected override _doLayout(heightInPixel: number): void { - const width = Math.min(640, this._availableSpaceGivenIndentation(this._indentationWidth)); + + const info = this.editor.getLayoutInfo(); + let width = info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth); + width = Math.min(640, width); + this._dimension = new Dimension(width, heightInPixel); this.widget.layout(this._dimension); } - private _availableSpaceGivenIndentation(indentationWidth: number | undefined): number { - const info = this.editor.getLayoutInfo(); - return info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth + (indentationWidth ?? 0)); - } - - private _computeHeightInLines(): number { + private _computeHeight(): { linesValue: number; pixelsValue: number } { const chatContentHeight = this.widget.contentHeight; const editorHeight = this.editor.getLayoutInfo().height; const contentHeight = Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42)); const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight); - return heightInLines; + return { linesValue: heightInLines, pixelsValue: contentHeight }; } protected override _onWidth(_widthInPixel: number): void { @@ -144,71 +159,69 @@ export class InlineChatZoneWidget extends ZoneWidget { const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; this.container.style.marginLeft = `${marginWithoutIndentation}px`; - super.show(position, this._computeHeightInLines()); - this._setWidgetMargins(position); + const height = this._computeHeight(); + super.show(position, height.linesValue); + this.widget.chatWidget.setVisible(true); this.widget.focus(); scrollState.restore(this.editor); - this.editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(position.delta(-1)), ScrollType.Immediate); + + this._revealTopOfZoneWidget(position, height); } override updatePositionAndHeight(position: Position): void { - super.updatePositionAndHeight(position, this._computeHeightInLines()); - this._setWidgetMargins(position); + const scrollState = StableEditorBottomScrollState.capture(this.editor); + const height = this._computeHeight(); + super.updatePositionAndHeight(position, height.linesValue); + scrollState.restore(this.editor); + + this._revealTopOfZoneWidget(position, height); + } + + private _revealTopOfZoneWidget(position: Position, height: { linesValue: number; pixelsValue: number }) { + + // reveal top of zone widget + + const lineNumber = position.lineNumber <= 1 ? 1 : 1 + position.lineNumber; + + const scrollTop = this.editor.getScrollTop(); + const lineTop = this.editor.getTopForLineNumber(lineNumber); + const zoneTop = lineTop - height.pixelsValue; + + const editorHeight = this.editor.getLayoutInfo().height; + const lineBottom = this.editor.getBottomForLineNumber(lineNumber); + + let newScrollTop = zoneTop; + let forceScrollTop = false; + + if (lineBottom >= (scrollTop + editorHeight)) { + // revealing the top of the zone would pust out the line we are interested it and + // therefore we keep the line in the view port + newScrollTop = lineBottom - editorHeight; + forceScrollTop = true; + } + + if (newScrollTop < scrollTop || forceScrollTop) { + this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); + this.editor.setScrollTop(newScrollTop, ScrollType.Immediate); + } + } + + protected override revealRange(range: Range, isLastLine: boolean): void { + // noop } protected override _getWidth(info: EditorLayoutInfo): number { return info.width - info.minimap.minimapWidth; } - updateBackgroundColor(newPosition: Position, wholeRange: IRange) { - assertType(this.container); - const widgetLineNumber = newPosition.lineNumber; - this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber); - } - - private _calculateIndentationWidth(position: Position): number { - const viewModel = this.editor._getViewModel(); - if (!viewModel) { - return 0; - } - - const visibleRange = viewModel.getCompletelyVisibleViewRange(); - if (!visibleRange.containsPosition(position)) { - // this is needed because `getOffsetForColumn` won't work when the position - // isn't visible/rendered - return 0; - } - - let indentationLevel = viewModel.getLineFirstNonWhitespaceColumn(position.lineNumber); - let indentationLineNumber = position.lineNumber; - for (let lineNumber = position.lineNumber; lineNumber >= visibleRange.startLineNumber; lineNumber--) { - const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber); - if (currentIndentationLevel !== 0) { - indentationLineNumber = lineNumber; - indentationLevel = currentIndentationLevel; - break; - } - } - - return Math.max(0, this.editor.getOffsetForColumn(indentationLineNumber, indentationLevel)); // double-guard against invalie getOffsetForColumn-calls - } - - private _setWidgetMargins(position: Position): void { - const indentationWidth = this._calculateIndentationWidth(position); - if (this._indentationWidth === indentationWidth) { - return; - } - this._indentationWidth = this._availableSpaceGivenIndentation(indentationWidth) > 400 ? indentationWidth : 0; - this.widget.domNode.style.marginLeft = `${this._indentationWidth}px`; - this.widget.domNode.style.marginRight = `${this.editor.getLayoutInfo().minimap.minimapWidth}px`; - } - override hide(): void { - this.container!.classList.remove('inside-selection'); + const scrollState = StableEditorBottomScrollState.capture(this.editor); this._ctxCursorPosition.reset(); this.widget.reset(); + this.widget.chatWidget.setVisible(false); super.hide(); aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); + scrollState.restore(this.editor); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index e7b6f8a6c0b..a6b855ff046 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -11,20 +11,18 @@ max-width: unset; } -.monaco-workbench .zone-widget-container.inside-selection { - background-color: var(--vscode-inlineChat-regionHighlight); -} - .monaco-workbench .inline-chat { color: inherit; - padding: 0 8px 8px 8px; border-radius: 4px; border: 1px solid var(--vscode-inlineChat-border); box-shadow: 0 2px 4px 0 var(--vscode-widget-shadow); - margin-top: 8px; background: var(--vscode-inlineChat-background); } +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part { + padding: 4px 6px 0 6px; +} + .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-execute-toolbar { margin-bottom: 1px; } @@ -39,35 +37,39 @@ } .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.interactive-item-compact { - padding: 6px 4px; gap: 6px; + padding-top: 2px; + padding-right: 20px; + padding-left: 6px; } .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.interactive-item-compact .header .avatar { outline-offset: -1px; } +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.interactive-item-compact .chat-notification-widget { + margin-bottom: 0; + padding: 0; + border: none; +} + .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-request { border: none; } -/* progress bit */ - -.monaco-workbench .inline-chat .progress { - position: relative; +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.minimal > .header { + top: 5px; + right: 10px; } -/* UGLY - fighting against workbench styles */ -.monaco-workbench .part.editor > .content .inline-chat .progress .monaco-progress-container { - top: 0; -} /* status */ -.monaco-workbench .inline-chat .status { +.monaco-workbench .inline-chat > .status { display: flex; justify-content: space-between; align-items: center; + padding: 4px 6px 0 6px } .monaco-workbench .inline-chat .status .actions.hidden { @@ -77,8 +79,9 @@ .monaco-workbench .inline-chat .status .label { overflow: hidden; color: var(--vscode-descriptionForeground); - font-size: 12px; - display: inline-flex; + font-size: 11px; + display: flex; + white-space: nowrap; } .monaco-workbench .inline-chat .status .label.info { @@ -103,50 +106,67 @@ } .monaco-workbench .inline-chat .status .label > .codicon { - padding: 0 5px; + padding: 0 3px; font-size: 12px; line-height: 18px; } -.monaco-workbench .inline-chat .chatMessage .chatMessageContent .value { - overflow: hidden; - -webkit-user-select: text; - user-select: text; -} -.monaco-workbench .inline-chat .followUps { - padding: 5px 5px; -} +.monaco-workbench .inline-chat .status .actions, +.monaco-workbench .inline-chat-content-widget .toolbar { -.monaco-workbench .inline-chat .followUps .interactive-session-followups .monaco-button { - display: block; - color: var(--vscode-textLink-foreground); - font-size: 12px; -} - -.monaco-workbench .inline-chat .followUps.hidden { - display: none; -} - -.monaco-workbench .inline-chat .chatMessage { - padding: 0 3px; -} - -.monaco-workbench .inline-chat .chatMessage .chatMessageContent { - padding: 2px 2px; -} - -.monaco-workbench .inline-chat .chatMessage.hidden { - display: none; -} - -.monaco-workbench .inline-chat .status .actions { display: flex; + height: 18px; + + .actions-container { + gap: 3px + } + + .action-item.text-only .action-label { + font-size: 12px; + line-height: 16px; + padding: 0 4px; + border-radius: 2px; + } + + .monaco-action-bar .action-item.menu-entry.text-only + .action-item:not(.text-only) > .monaco-dropdown .action-label { + font-size: 12px; + line-height: 16px; + width: unset; + height: unset; + } +} + +.monaco-workbench .inline-chat .status .actions, +.monaco-workbench .inline-chat-content-widget.contents .toolbar { + + .monaco-action-bar .action-item.menu-entry.text-only:first-of-type .action-label{ + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + } +} + +.monaco-workbench .inline-chat .status { + .actions.text-style { + display: none; + } + .actions.button-style { + display: inherit; + } +} + +.monaco-workbench .inline-chat .status.text { + .actions.text-style { + display: inherit; + } + .actions.button-style { + display: none; + } } .monaco-workbench .inline-chat .status .actions > .monaco-button, .monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown { - margin-right: 6px; + margin-right: 4px; } .monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { @@ -164,12 +184,8 @@ } .monaco-workbench .inline-chat .status .actions .monaco-text-button { - padding: 2px 4px; - white-space: nowrap; -} - -.monaco-workbench .inline-chat .status .monaco-toolbar .action-item { padding: 0 2px; + white-space: nowrap; } /* TODO@jrieken not needed? */ @@ -184,45 +200,17 @@ background-color: var(--vscode-button-hoverBackground); } -/* preview */ +/* accessible diff viewer */ -.monaco-workbench .inline-chat .preview { +.monaco-workbench .inline-chat .diff-review { + padding: 4px 6px; + background-color: unset; +} + +.monaco-workbench .inline-chat .diff-review.hidden { display: none; } -.monaco-workbench .inline-chat .previewDiff, -.monaco-workbench .inline-chat .previewCreate { - display: inherit; - border: 1px solid var(--vscode-inlineChat-border); - border-radius: 2px; - margin: 6px 0px; -} - -.monaco-workbench .inline-chat .previewCreateTitle { - padding-top: 6px; -} - -.monaco-workbench .inline-chat .diff-review.hidden, -.monaco-workbench .inline-chat .previewDiff.hidden, -.monaco-workbench .inline-chat .previewCreate.hidden, -.monaco-workbench .inline-chat .previewCreateTitle.hidden { - display: none; -} - -.monaco-workbench .inline-chat-toolbar { - display: flex; -} - -.monaco-workbench .inline-chat-toolbar > .monaco-button { - margin-right: 6px; -} - -.monaco-workbench .inline-chat-toolbar .action-label.checked { - color: var(--vscode-inputOption-activeForeground); - background-color: var(--vscode-inputOption-activeBackground); - outline: 1px solid var(--vscode-inputOption-activeBorder); -} - /* decoration styles */ .monaco-workbench .inline-chat-inserted-range { @@ -246,51 +234,6 @@ background-color: var(--vscode-inlineChat-regionHighlight); } -.monaco-workbench .interactive-session .interactive-input-and-execute-toolbar .monaco-editor .inline-chat-slash-command { - background-color: var(--vscode-chat-slashCommandBackground); - color: var(--vscode-chat-slashCommandForeground); /* Overrides the foreground color rule in chat.css */ - border-radius: 2px; - padding: 1px; -} - -.monaco-workbench .inline-chat-slash-command-detail { - opacity: 0.5; -} - -/* diff zone */ - -.monaco-workbench .inline-chat-diff-widget .monaco-diff-editor .monaco-editor-background, -.monaco-workbench .inline-chat-diff-widget .monaco-diff-editor .monaco-workbench .margin-view-overlays { - background-color: var(--vscode-inlineChat-regionHighlight); -} - -/* create zone */ - -.monaco-workbench .inline-chat-newfile-widget { - background-color: var(--vscode-inlineChat-regionHighlight); -} - -.monaco-workbench .inline-chat-newfile-widget .title { - display: flex; - align-items: center; - justify-content: space-between; -} - -.monaco-workbench .inline-chat-newfile-widget .title .detail { - margin-left: 4px; -} - -.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget { - display: flex; - margin-left: auto; - margin-right: 8px; -} - -.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget > .monaco-button { - display: inline-flex; - white-space: nowrap; - margin-left: 4px; -} /* gutter decoration */ diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css index bc9e18e6ea6..7da5cc3e97e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css @@ -5,15 +5,12 @@ .monaco-workbench .inline-chat-content-widget { z-index: 50; - padding: 6px 6px 6px 6px; + padding: 6px; border-radius: 4px; background-color: var(--vscode-inlineChat-background); box-shadow: 0 4px 8px var(--vscode-inlineChat-shadow); } -.monaco-workbench .inline-chat-content-widget .hidden { - display: none; -} .monaco-workbench .inline-chat-content-widget.interactive-session .interactive-session { max-width: unset; @@ -27,15 +24,11 @@ padding: 0; } -.monaco-workbench .inline-chat-content-widget .message { - overflow: hidden; - color: var(--vscode-descriptionForeground); - font-size: 11px; - display: inline-flex; +.monaco-workbench .inline-chat-content-widget.interactive-session .interactive-list { + display: none; } -.monaco-workbench .inline-chat-content-widget .message > .codicon { - padding-right: 5px; - font-size: 12px; - line-height: 18px; +.monaco-workbench .inline-chat-content-widget.interactive-session .toolbar { + display: none; + padding-top: 4px; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/utils.ts b/src/vs/workbench/contrib/inlineChat/browser/utils.ts index 0460ee11f02..2e593a6ee5f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/utils.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/utils.ts @@ -64,6 +64,9 @@ export function asProgressiveEdit(interval: IntervalTimer, edit: IIdentifiedSing let newText = edit.text ?? ''; interval.cancelAndSet(() => { + if (token.isCancellationRequested) { + return; + } const r = getNWords(newText, 1); stream.emitOne(r.value); newText = newText.substring(r.value.length); diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 241986e3f3b..b4c754b1759 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -3,141 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { IRange } from 'vs/editor/common/core/range'; -import { TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; -import { diffInserted, diffRemoved, editorHoverHighlight, editorWidgetBackground, editorWidgetBorder, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, transparent, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { Extensions as ExtensionsMigration, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; -import { IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents'; - -export interface IInlineChatSession { - id: number; - placeholder?: string; - input?: string; - message?: string; - slashCommands?: IChatAgentCommand[]; - wholeRange?: IRange; -} - -export type IInlineChatResponse = IInlineChatEditResponse | IInlineChatBulkEditResponse; - -export const enum InlineChatResponseType { - EditorEdit = 'editorEdit', - BulkEdit = 'bulkEdit' -} - -export const enum InlineChatResponseTypes { - Empty = 'empty', - OnlyEdits = 'onlyEdits', - OnlyMessages = 'onlyMessages', - Mixed = 'mixed' -} - -export interface IInlineChatEditResponse { - id: number; - type: InlineChatResponseType.EditorEdit; - edits: TextEdit[]; - message?: IMarkdownString; - placeholder?: string; - wholeRange?: IRange; -} - -export interface IInlineChatBulkEditResponse { - id: number; - type: InlineChatResponseType.BulkEdit; - edits: WorkspaceEdit; - message?: IMarkdownString; - placeholder?: string; - wholeRange?: IRange; -} - - -export const INLINE_CHAT_ID = 'interactiveEditor'; -export const INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID = 'interactiveEditorAccessiblityHelp'; - -export const enum EditMode { - Live = 'live', - Preview = 'preview' -} - -export const CTX_INLINE_CHAT_HAS_PROVIDER = new RawContextKey('inlineChatHasProvider', false, localize('inlineChatHasProvider', "Whether a provider for interactive editors exists")); -export const CTX_INLINE_CHAT_VISIBLE = new RawContextKey('inlineChatVisible', false, localize('inlineChatVisible', "Whether the interactive editor input is visible")); -export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey('inlineChatFocused', false, localize('inlineChatFocused', "Whether the interactive editor input is focused")); -export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey('inlineChatResponseFocused', false, localize('inlineChatResponseFocused', "Whether the interactive widget's response is focused")); -export const CTX_INLINE_CHAT_EMPTY = new RawContextKey('inlineChatEmpty', false, localize('inlineChatEmpty', "Whether the interactive editor input is empty")); -export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('inlineChatInnerCursorFirst', false, localize('inlineChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line")); -export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); -export const CTX_INLINE_CHAT_INNER_CURSOR_START = new RawContextKey('inlineChatInnerCursorStart', false, localize('inlineChatInnerCursorStart', "Whether the cursor of the iteractive editor input is on the start of the input")); -export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inlineChatInnerCursorEnd', false, localize('inlineChatInnerCursorEnd', "Whether the cursor of the iteractive editor input is on the end of the input")); -export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); -export const CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey('inlineChatHasActiveRequest', false, localize('inlineChatHasActiveRequest', "Whether interactive editor has an active request")); -export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); -export const CTX_INLINE_CHAT_LAST_RESPONSE_TYPE = new RawContextKey('inlineChatLastResponseType', undefined, localize('inlineChatResponseType', "What type was the last response of the current interactive editor session")); -export const CTX_INLINE_CHAT_RESPONSE_TYPES = new RawContextKey('inlineChatResponseTypes', InlineChatResponseTypes.Empty, localize('inlineChatResponseTypes', "What type was the responses have been receieved")); -export const CTX_INLINE_CHAT_DID_EDIT = new RawContextKey('inlineChatDidEdit', undefined, localize('inlineChatDidEdit', "Whether interactive editor did change any code")); -export const CTX_INLINE_CHAT_USER_DID_EDIT = new RawContextKey('inlineChatUserDidEdit', undefined, localize('inlineChatUserDidEdit', "Whether the user did changes ontop of the inline chat")); -export const CTX_INLINE_CHAT_LAST_FEEDBACK = new RawContextKey<'unhelpful' | 'helpful' | ''>('inlineChatLastFeedbackKind', '', localize('inlineChatLastFeedbackKind', "The last kind of feedback that was provided")); -export const CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING = new RawContextKey('inlineChatSupportIssueReporting', false, localize('inlineChatSupportIssueReporting', "Whether the interactive editor supports issue reporting")); -export const CTX_INLINE_CHAT_DOCUMENT_CHANGED = new RawContextKey('inlineChatDocumentChanged', false, localize('inlineChatDocumentChanged', "Whether the document has changed concurrently")); -export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlineChatChangeHasDiff', false, localize('inlineChatChangeHasDiff', "Whether the current change supports showing a diff")); -export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineChatChangeShowsDiff', false, localize('inlineChatChangeShowsDiff', "Whether the current change showing a diff")); -export const CTX_INLINE_CHAT_EDIT_MODE = new RawContextKey('config.inlineChat.mode', EditMode.Live); - -// --- (select) action identifier - -export const ACTION_ACCEPT_CHANGES = 'inlineChat.acceptChanges'; -export const ACTION_REGENERATE_RESPONSE = 'inlineChat.regenerate'; -export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat'; -export const ACTION_TOGGLE_DIFF = 'inlineChat.toggleDiff'; - -// --- menus - -export const MENU_INLINE_CHAT_WIDGET = MenuId.for('inlineChatWidget'); -export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.status'); - -// --- colors - - -export const inlineChatBackground = registerColor('inlineChat.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, localize('inlineChat.background', "Background color of the interactive editor widget")); -export const inlineChatBorder = registerColor('inlineChat.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('inlineChat.border', "Border color of the interactive editor widget")); -export const inlineChatShadow = registerColor('inlineChat.shadow', { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, localize('inlineChat.shadow', "Shadow color of the interactive editor widget")); -export const inlineChatRegionHighlight = registerColor('inlineChat.regionHighlight', { dark: editorHoverHighlight, light: editorHoverHighlight, hcDark: editorHoverHighlight, hcLight: editorHoverHighlight }, localize('inlineChat.regionHighlight', "Background highlighting of the current interactive region. Must be transparent."), true); -export const inlineChatInputBorder = registerColor('inlineChatInput.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('inlineChatInput.border', "Border color of the interactive editor input")); -export const inlineChatInputFocusBorder = registerColor('inlineChatInput.focusBorder', { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, localize('inlineChatInput.focusBorder', "Border color of the interactive editor input when focused")); -export const inlineChatInputPlaceholderForeground = registerColor('inlineChatInput.placeholderForeground', { dark: inputPlaceholderForeground, light: inputPlaceholderForeground, hcDark: inputPlaceholderForeground, hcLight: inputPlaceholderForeground }, localize('inlineChatInput.placeholderForeground', "Foreground color of the interactive editor input placeholder")); -export const inlineChatInputBackground = registerColor('inlineChatInput.background', { dark: inputBackground, light: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('inlineChatInput.background', "Background color of the interactive editor input")); - -export const inlineChatDiffInserted = registerColor('inlineChatDiff.inserted', { dark: transparent(diffInserted, .5), light: transparent(diffInserted, .5), hcDark: transparent(diffInserted, .5), hcLight: transparent(diffInserted, .5) }, localize('inlineChatDiff.inserted', "Background color of inserted text in the interactive editor input")); -export const overviewRulerInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); -export const minimapInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); - -export const inlineChatDiffRemoved = registerColor('inlineChatDiff.removed', { dark: transparent(diffRemoved, .5), light: transparent(diffRemoved, .5), hcDark: transparent(diffRemoved, .5), hcLight: transparent(diffRemoved, .5) }, localize('inlineChatDiff.removed', "Background color of removed text in the interactive editor input")); -export const overviewRulerInlineChatDiffRemoved = registerColor('editorOverviewRuler.inlineChatRemoved', { dark: transparent(diffRemoved, 0.6), light: transparent(diffRemoved, 0.8), hcDark: transparent(diffRemoved, 0.6), hcLight: transparent(diffRemoved, 0.8) }, localize('editorOverviewRuler.inlineChatRemoved', 'Overview ruler marker color for inline chat removed content.')); - +import { diffInserted, diffRemoved, editorWidgetBackground, editorWidgetBorder, editorWidgetForeground, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, transparent, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; // settings - - -Registry.as(ExtensionsMigration.ConfigurationMigration).registerConfigurationMigrations( - [{ - key: 'interactiveEditor.editMode', migrateFn: (value: any) => { - return [['inlineChat.mode', { value: value }]]; - } - }] -); - export const enum InlineChatConfigKeys { Mode = 'inlineChat.mode', FinishOnType = 'inlineChat.finishOnType', AcceptedOrDiscardBeforeSave = 'inlineChat.acceptedOrDiscardBeforeSave', HoldToSpeech = 'inlineChat.holdToSpeech', - AccessibleDiffView = 'inlineChat.accessibleDiffView' + AccessibleDiffView = 'inlineChat.accessibleDiffView', + ExpTextButtons = 'inlineChat.experimental.textButtons' +} + +export const enum EditMode { + Live = 'live', + Preview = 'preview' } Registry.as(Extensions.Configuration).registerConfiguration({ @@ -179,6 +65,77 @@ Registry.as(Extensions.Configuration).registerConfigurat localize('accessibleDiffView.on', "The accessible diff viewer is always enabled."), localize('accessibleDiffView.off', "The accessible diff viewer is never enabled."), ], - } + }, + [InlineChatConfigKeys.ExpTextButtons]: { + description: localize('txtButtons', "Whether to use textual buttons."), + default: false, + type: 'boolean', + tags: ['experimental'] + }, } }); + + +export const INLINE_CHAT_ID = 'interactiveEditor'; +export const INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID = 'interactiveEditorAccessiblityHelp'; + +// --- CONTEXT + +export const enum InlineChatResponseType { + None = 'none', + Messages = 'messages', + MessagesAndEdits = 'messagesAndEdits' +} + +export const CTX_INLINE_CHAT_HAS_AGENT = new RawContextKey('inlineChatHasProvider', false, localize('inlineChatHasProvider', "Whether a provider for interactive editors exists")); +export const CTX_INLINE_CHAT_VISIBLE = new RawContextKey('inlineChatVisible', false, localize('inlineChatVisible', "Whether the interactive editor input is visible")); +export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey('inlineChatFocused', false, localize('inlineChatFocused', "Whether the interactive editor input is focused")); +export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey('inlineChatResponseFocused', false, localize('inlineChatResponseFocused', "Whether the interactive widget's response is focused")); +export const CTX_INLINE_CHAT_EMPTY = new RawContextKey('inlineChatEmpty', false, localize('inlineChatEmpty', "Whether the interactive editor input is empty")); +export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('inlineChatInnerCursorFirst', false, localize('inlineChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line")); +export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); +export const CTX_INLINE_CHAT_INNER_CURSOR_START = new RawContextKey('inlineChatInnerCursorStart', false, localize('inlineChatInnerCursorStart', "Whether the cursor of the iteractive editor input is on the start of the input")); +export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inlineChatInnerCursorEnd', false, localize('inlineChatInnerCursorEnd', "Whether the cursor of the iteractive editor input is on the end of the input")); +export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); +export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); +export const CTX_INLINE_CHAT_USER_DID_EDIT = new RawContextKey('inlineChatUserDidEdit', undefined, localize('inlineChatUserDidEdit', "Whether the user did changes ontop of the inline chat")); +export const CTX_INLINE_CHAT_DOCUMENT_CHANGED = new RawContextKey('inlineChatDocumentChanged', false, localize('inlineChatDocumentChanged', "Whether the document has changed concurrently")); +export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlineChatChangeHasDiff', false, localize('inlineChatChangeHasDiff', "Whether the current change supports showing a diff")); +export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineChatChangeShowsDiff', false, localize('inlineChatChangeShowsDiff', "Whether the current change showing a diff")); +export const CTX_INLINE_CHAT_EDIT_MODE = new RawContextKey('config.inlineChat.mode', EditMode.Live); +export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('inlineChatRequestInProgress', false, localize('inlineChatRequestInProgress', "Whether an inline chat request is currently in progress")); +export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey('inlineChatResponseType', InlineChatResponseType.None, localize('inlineChatResponseTypes', "What type was the responses have been receieved, nothing yet, just messages, or messaged and local edits")); + +export const CTX_INLINE_CHAT_CONFIG_TXT_BTNS = ContextKeyExpr.equals(`config.${[InlineChatConfigKeys.ExpTextButtons]}`, true); + +// --- (selected) action identifier + +export const ACTION_ACCEPT_CHANGES = 'inlineChat.acceptChanges'; +export const ACTION_REGENERATE_RESPONSE = 'inlineChat.regenerate'; +export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat'; +export const ACTION_TOGGLE_DIFF = 'inlineChat.toggleDiff'; + +// --- menus + +export const MENU_INLINE_CHAT_EXECUTE = MenuId.for('inlineChat.execute'); +export const MENU_INLINE_CHAT_CONTENT_STATUS = MenuId.for('inlineChat.content.status'); +export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.status'); + +// --- colors + + +export const inlineChatForeground = registerColor('inlineChat.foreground', editorWidgetForeground, localize('inlineChat.foreground', "Foreground color of the interactive editor widget")); +export const inlineChatBackground = registerColor('inlineChat.background', editorWidgetBackground, localize('inlineChat.background', "Background color of the interactive editor widget")); +export const inlineChatBorder = registerColor('inlineChat.border', editorWidgetBorder, localize('inlineChat.border', "Border color of the interactive editor widget")); +export const inlineChatShadow = registerColor('inlineChat.shadow', widgetShadow, localize('inlineChat.shadow', "Shadow color of the interactive editor widget")); +export const inlineChatInputBorder = registerColor('inlineChatInput.border', editorWidgetBorder, localize('inlineChatInput.border', "Border color of the interactive editor input")); +export const inlineChatInputFocusBorder = registerColor('inlineChatInput.focusBorder', focusBorder, localize('inlineChatInput.focusBorder', "Border color of the interactive editor input when focused")); +export const inlineChatInputPlaceholderForeground = registerColor('inlineChatInput.placeholderForeground', inputPlaceholderForeground, localize('inlineChatInput.placeholderForeground', "Foreground color of the interactive editor input placeholder")); +export const inlineChatInputBackground = registerColor('inlineChatInput.background', inputBackground, localize('inlineChatInput.background', "Background color of the interactive editor input")); + +export const inlineChatDiffInserted = registerColor('inlineChatDiff.inserted', transparent(diffInserted, .5), localize('inlineChatDiff.inserted', "Background color of inserted text in the interactive editor input")); +export const overviewRulerInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); +export const minimapInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); + +export const inlineChatDiffRemoved = registerColor('inlineChatDiff.removed', transparent(diffRemoved, .5), localize('inlineChatDiff.removed', "Background color of removed text in the interactive editor input")); +export const overviewRulerInlineChatDiffRemoved = registerColor('editorOverviewRuler.inlineChatRemoved', { dark: transparent(diffRemoved, 0.6), light: transparent(diffRemoved, 0.8), hcDark: transparent(diffRemoved, 0.6), hcLight: transparent(diffRemoved, 0.8) }, localize('editorOverviewRuler.inlineChatRemoved', 'Overview ruler marker color for inline chat removed content.')); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 497e80085f2..cc4ddad6e7a 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { equals } from 'vs/base/common/arrays'; -import { timeout } from 'vs/base/common/async'; +import { DeferredPromise, raceCancellation, timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; @@ -14,7 +14,7 @@ import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; -import { ITextModel } from 'vs/editor/common/model'; +import { EndOfLineSequence, ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IModelService } from 'vs/editor/common/services/model'; import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService'; @@ -27,22 +27,22 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/common/progress'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IView, IViewDescriptorService } from 'vs/workbench/common/views'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; -import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAgentLocation, ChatAgentService, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, InlineChatConfigKeys } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestViewsService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInlineChatSavingService } from '../../browser/inlineChatSavingService'; import { IInlineChatSessionService } from '../../browser/inlineChatSessionService'; import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl'; import { TestWorkerService } from './testWorkerService'; import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; @@ -59,6 +59,10 @@ import { NullHoverService } from 'vs/platform/hover/test/browser/nullHoverServic import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; +import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { RerunAction } from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { assertType } from 'vs/base/common/types'; suite('InteractiveChatController', function () { @@ -85,21 +89,21 @@ suite('InteractiveChatController', function () { readonly states: readonly State[] = []; - waitFor(states: readonly State[]): Promise { + awaitStates(states: readonly State[]): Promise { const actual: State[] = []; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const d = this.onDidChangeState(state => { actual.push(state); if (equals(states, actual)) { d.dispose(); - resolve(); + resolve(undefined); } }); setTimeout(() => { d.dispose(); - reject(new Error(`timeout, \nEXPECTED: ${states.join('>')}, \nACTUAL : ${actual.join('>')}`)); + resolve(`[${states.join(',')}] <> [${actual.join(',')}]`); }, 1000); }); } @@ -125,10 +129,13 @@ suite('InteractiveChatController', function () { let model: ITextModel; let ctrl: TestController; let contextKeyService: MockContextKeyService; + let chatService: IChatService; let chatAgentService: IChatAgentService; let inlineChatSessionService: IInlineChatSessionService; let instaService: TestInstantiationService; + let chatWidget: IChatWidget; + setup(function () { const serviceCollection = new ServiceCollection( @@ -139,12 +146,21 @@ suite('InteractiveChatController', function () { [IHoverService, NullHoverService], [IExtensionService, new TestExtensionService()], [IContextKeyService, new MockContextKeyService()], - [IViewsService, new TestExtensionService()], + [IViewsService, new class extends TestViewsService { + override async openView(id: string, focus?: boolean | undefined): Promise { + return { widget: chatWidget ?? null } as any; + } + }()], [IWorkspaceContextService, new TestContextService()], [IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)], [IChatWidgetService, new SyncDescriptor(ChatWidgetService)], [IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)], [IChatService, new SyncDescriptor(ChatService)], + [IChatAgentNameService, new class extends mock() { + override getAgentNameRestriction(chatAgentData: IChatAgentData): boolean { + return false; + } + }], [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IContextKeyService, contextKeyService], [IChatAgentService, new SyncDescriptor(ChatAgentService)], @@ -177,6 +193,9 @@ suite('InteractiveChatController', function () { [IConfigurationService, configurationService], [IViewDescriptorService, new class extends mock() { override onDidChangeLocation = Event.None; + }], + [INotebookEditorService, new class extends mock() { + override listNotebookEditors() { return []; } }] ); @@ -188,12 +207,13 @@ suite('InteractiveChatController', function () { configurationService.setUserConfiguration('editor', {}); contextKeyService = instaService.get(IContextKeyService) as MockContextKeyService; - + chatService = instaService.get(IChatService); chatAgentService = instaService.get(IChatAgentService); inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); + model.setEOL(EndOfLineSequence.LF); editor = store.add(instantiateTestCodeEditor(instaService, model)); store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, { @@ -228,9 +248,9 @@ suite('InteractiveChatController', function () { test('run (show/hide)', async function () { ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); + const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND); const run = ctrl.run({ message: 'Hello', autoSend: true }); - await p; + assert.strictEqual(await actualStates, undefined); assert.ok(ctrl.getWidgetPosition() !== undefined); await ctrl.cancelSession(); @@ -259,10 +279,10 @@ suite('InteractiveChatController', function () { configurationService.setUserConfiguration(InlineChatConfigKeys.FinishOnType, true); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); + const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND); const r = ctrl.run({ message: 'Hello', autoSend: true }); - await p; + assert.strictEqual(await actualStates, undefined); const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); assert.ok(session); @@ -271,7 +291,7 @@ suite('InteractiveChatController', function () { editor.setSelection(new Range(2, 1, 2, 1)); editor.trigger('test', 'type', { text: 'a' }); - await ctrl.waitFor([State.ACCEPT]); + assert.strictEqual(await ctrl.awaitStates([State.ACCEPT]), undefined); await r; }); @@ -298,17 +318,19 @@ suite('InteractiveChatController', function () { })); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor(TestController.INIT_SEQUENCE); + const p = ctrl.awaitStates(TestController.INIT_SEQUENCE); const r = ctrl.run({ message: 'GENGEN', autoSend: false }); - await p; + assert.strictEqual(await p, undefined); + const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); assert.ok(session); assert.deepStrictEqual(session.wholeRange.value, new Range(3, 1, 3, 3)); // initial + ctrl.chatWidget.setInput('GENGEN'); ctrl.acceptInput(); - await ctrl.waitFor([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + assert.strictEqual(await ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]), undefined); assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 4, 3)); @@ -328,10 +350,11 @@ suite('InteractiveChatController', function () { })); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); const r = ctrl.run({ message: 'Hello', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); + ctrl.acceptSession(); await r; @@ -357,9 +380,9 @@ suite('InteractiveChatController', function () { const valueThen = editor.getModel().getValue(); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'Hello', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); ctrl.acceptSession(); await r; @@ -400,9 +423,9 @@ suite('InteractiveChatController', function () { // store.add(editor.getModel().onDidChangeContent(() => { modelChangeCounter++; })); ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'Hello', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); // assert.ok(modelChangeCounter > 0, modelChangeCounter.toString()); // some changes have been made // const modelChangeCounterNow = modelChangeCounter; @@ -423,9 +446,9 @@ suite('InteractiveChatController', function () { // NO manual edits -> cancel ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'GENERATED', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); assert.ok(model.getValue().includes('GENERATED')); assert.strictEqual(contextKeyService.getContextKeyValue(CTX_INLINE_CHAT_USER_DID_EDIT.key), undefined); @@ -439,9 +462,9 @@ suite('InteractiveChatController', function () { // manual edits -> finish ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); const r = ctrl.run({ message: 'GENERATED', autoSend: true }); - await p; + assert.strictEqual(await p, undefined); assert.ok(model.getValue().includes('GENERATED')); @@ -454,4 +477,348 @@ suite('InteractiveChatController', function () { assert.ok(model.getValue().includes('MANUAL')); }); + + test('re-run should discard pending edits', async function () { + + let count = 1; + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }); + return {}; + }, + })); + + ctrl = instaService.createInstance(TestController, editor); + const rerun = new RerunAction(); + + model.setValue(''); + + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const r = ctrl.run({ message: 'PROMPT_', autoSend: true }); + assert.strictEqual(await p, undefined); + + + assert.strictEqual(model.getValue(), 'PROMPT_1'); + + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); + + assert.strictEqual(await p2, undefined); + + assert.strictEqual(model.getValue(), 'PROMPT_2'); + ctrl.finishExistingSession(); + await r; + }); + + test('Retry undoes all changes, not just those from the request#5736', async function () { + + const text = [ + 'eins-', + 'zwei-', + 'drei-' + ]; + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }); + return {}; + }, + })); + + ctrl = instaService.createInstance(TestController, editor); + const rerun = new RerunAction(); + + model.setValue(''); + + // REQUEST 1 + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const r = ctrl.run({ message: '1', autoSend: true }); + assert.strictEqual(await p, undefined); + + assert.strictEqual(model.getValue(), 'eins-'); + + // REQUEST 2 + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + ctrl.chatWidget.setInput('1'); + await ctrl.acceptInput(); + assert.strictEqual(await p2, undefined); + + assert.strictEqual(model.getValue(), 'zwei-eins-'); + + // REQUEST 2 - RERUN + const p3 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); + assert.strictEqual(await p3, undefined); + + assert.strictEqual(model.getValue(), 'drei-eins-'); + + ctrl.finishExistingSession(); + await r; + + }); + + test('moving inline chat to another model undoes changes', async function () { + const text = [ + 'eins\n', + 'zwei\n' + ]; + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }); + return {}; + }, + })); + ctrl = instaService.createInstance(TestController, editor); + + // REQUEST 1 + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + ctrl.run({ message: '1', autoSend: true }); + assert.strictEqual(await p, undefined); + + assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n'); + + const targetModel = chatService.startSession(ChatAgentLocation.Editor, CancellationToken.None)!; + store.add(targetModel); + chatWidget = new class extends mock() { + override get viewModel() { + return { model: targetModel } as any; + } + override focusLastMessage() { } + }; + + const r = ctrl.joinCurrentRun(); + await ctrl.viewInChat(); + + assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n'); + await r; + }); + + test('moving inline chat to another model undoes changes (2 requests)', async function () { + const text = [ + 'eins\n', + 'zwei\n' + ]; + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }); + return {}; + }, + })); + ctrl = instaService.createInstance(TestController, editor); + + // REQUEST 1 + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + ctrl.run({ message: '1', autoSend: true }); + assert.strictEqual(await p, undefined); + + assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n'); + + // REQUEST 2 + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + ctrl.chatWidget.setInput('1'); + await ctrl.acceptInput(); + assert.strictEqual(await p2, undefined); + + assert.strictEqual(model.getValue(), 'zwei\neins\nHello\nWorld\nHello Again\nHello World\n'); + + const targetModel = chatService.startSession(ChatAgentLocation.Editor, CancellationToken.None)!; + store.add(targetModel); + chatWidget = new class extends mock() { + override get viewModel() { + return { model: targetModel } as any; + } + override focusLastMessage() { } + }; + + const r = ctrl.joinCurrentRun(); + + await ctrl.viewInChat(); + + assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n'); + + await r; + }); + + test('Clicking "re-run without /doc" while a request is in progress closes the widget #5997', async function () { + + model.setValue(''); + + let count = 0; + const commandDetection: (boolean | undefined)[] = []; + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + commandDetection.push(request.enableCommandDetection); + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }); + + if (count === 1) { + // FIRST call waits for cancellation + await raceCancellation(new Promise(() => { }), token); + } else { + await timeout(10); + } + + return {}; + }, + })); + ctrl = instaService.createInstance(TestController, editor); + + // REQUEST 1 + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); + ctrl.run({ message: 'Hello-', autoSend: true }); + assert.strictEqual(await p, undefined); + + // resend pending request without command detection + const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1); + assertType(request); + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE]); + chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.Editor }); + + assert.strictEqual(await p2, undefined); + + assert.deepStrictEqual(commandDetection, [true, false]); + assert.strictEqual(model.getValue(), 'Hello-1'); + }); + + test('Re-run without after request is done', async function () { + + model.setValue(''); + + let count = 0; + const commandDetection: (boolean | undefined)[] = []; + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + commandDetection.push(request.enableCommandDetection); + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }); + return {}; + }, + })); + ctrl = instaService.createInstance(TestController, editor); + + // REQUEST 1 + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + ctrl.run({ message: 'Hello-', autoSend: true }); + assert.strictEqual(await p, undefined); + + // resend pending request without command detection + const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1); + assertType(request); + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.Editor }); + + assert.strictEqual(await p2, undefined); + + assert.deepStrictEqual(commandDetection, [true, false]); + assert.strictEqual(model.getValue(), 'Hello-1'); + }); + + + test('Inline: Pressing Rerun request while the response streams breaks the response #5442', async function () { + + model.setValue('two\none\n'); + + const attempts: (number | undefined)[] = []; + + const deferred = new DeferredPromise(); + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + + attempts.push(request.attempt); + + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: `TRY:${request.attempt}\n` }] }); + await raceCancellation(deferred.p, token); + deferred.complete(); + await timeout(10); + return {}; + }, + })); + + ctrl = instaService.createInstance(TestController, editor); + + // REQUEST 1 + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); + ctrl.run({ message: 'Hello-', autoSend: true }); + assert.strictEqual(await p, undefined); + assert.deepStrictEqual(attempts, [0]); + + // RERUN (cancel, undo, redo) + const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + const rerun = new RerunAction(); + await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); + assert.strictEqual(await p2, undefined); + + assert.deepStrictEqual(attempts, [0, 1]); + + assert.strictEqual(model.getValue(), 'TRY:1\ntwo\none\n'); + + }); + + test('Stopping/cancelling a request should undo its changes', async function () { + + model.setValue('World'); + + const deferred = new DeferredPromise(); + let progress: ((part: IChatProgress) => void) | undefined; + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, _progress, history, token) { + + progress = _progress; + await deferred.p; + return {}; + }, + })); + + ctrl = instaService.createInstance(TestController, editor); + + // REQUEST 1 + const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); + ctrl.run({ message: 'Hello', autoSend: true }); + assert.strictEqual(await p, undefined); + + assertType(progress); + + const modelChange = new Promise(resolve => model.onDidChangeContent(() => resolve())); + + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello-Hello' }] }); + + await modelChange; + assert.strictEqual(model.getValue(), 'HelloWorld'); // first word has been streamed + + const p2 = ctrl.awaitStates([State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]); + chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionId); + assert.strictEqual(await p2, undefined); + + assert.strictEqual(model.getValue(), 'World'); + + }); }); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 3731977e58b..26e72175e2c 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { mock } from 'vs/base/test/common/mock'; @@ -160,7 +160,7 @@ suite('InlineChatSession', function () { } finally { session.hunkData.ignoreTextModelNChanges = false; } - await session.hunkData.recompute(); + await session.hunkData.recompute({ applied: 0, sha1: 'fakeSha1' }); } function makeEdit(edit: EditOperation | EditOperation[]) { @@ -433,4 +433,51 @@ suite('InlineChatSession', function () { assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue()); }); + test('HunkData, accept, discardAll', async function () { + + const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + assertType(session); + + await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); + + assert.strictEqual(session.hunkData.size, 2); + assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); + + const textModeNNow = session.textModelN.getValue(); + + session.hunkData.getInfo()[0].acceptChanges(); + assert.strictEqual(textModeNNow, session.textModelN.getValue()); + + session.hunkData.discardAll(); // all remaining + assert.strictEqual(session.textModelN.getValue(), 'AI_EDIT\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven'); + assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue()); + + inlineChatSessionService.releaseSession(session); + }); + + test('HunkData, discardAll return undo edits', async function () { + + const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + assertType(session); + + await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); + + assert.strictEqual(session.hunkData.size, 2); + assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); + + const textModeNNow = session.textModelN.getValue(); + + session.hunkData.getInfo()[0].acceptChanges(); + assert.strictEqual(textModeNNow, session.textModelN.getValue()); + + const undoEdits = session.hunkData.discardAll(); // all remaining + assert.strictEqual(session.textModelN.getValue(), 'AI_EDIT\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven'); + assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue()); + + // undo the discards + session.textModelN.pushEditOperations(null, undoEdits, () => null); + assert.strictEqual(textModeNNow, session.textModelN.getValue()); + + inlineChatSessionService.releaseSession(session); + }); }); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts index e8d229d81fc..c6724b095e4 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts @@ -7,7 +7,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IntervalTimer } from 'vs/base/common/async'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { asProgressiveEdit } from '../../browser/utils'; -import * as assert from 'assert'; +import assert from 'assert'; suite('AsyncEdit', () => { diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index cc21b0371a6..2109f7ef705 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -23,6 +23,7 @@ import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/sug import { localize, localize2 } from 'vs/nls'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -54,6 +55,9 @@ import { CellEditType, CellKind, CellUri, INTERACTIVE_WINDOW_EDITOR_ID, Notebook import { InteractiveWindowOpen } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { executeReplInput } from 'vs/workbench/contrib/replNotebook/browser/repl.contribution'; +import { ReplEditor } from 'vs/workbench/contrib/replNotebook/browser/replEditor'; +import { ReplEditorInput } from 'vs/workbench/contrib/replNotebook/browser/replEditorInput'; import { columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; @@ -95,7 +99,7 @@ export class InteractiveDocumentContribution extends Disposable implements IWork providerDisplayName: 'Interactive Notebook', displayName: 'Interactive', filenamePattern: ['*.interactive'], - exclusive: true + priority: RegisteredEditorPriority.exclusive })); } @@ -411,10 +415,10 @@ registerAction2(class extends Action2 { logService.debug('Open new interactive window:', notebookUri.toString(), inputUri.toString()); if (id) { - const allKernels = kernelService.getMatchingKernel({ uri: notebookUri, viewType: 'interactive' }).all; + const allKernels = kernelService.getMatchingKernel({ uri: notebookUri, notebookType: 'interactive' }).all; const preferredKernel = allKernels.find(kernel => kernel.id === id); if (preferredKernel) { - kernelService.preselectKernelForNotebook(preferredKernel, { uri: notebookUri, viewType: 'interactive' }); + kernelService.preselectKernelForNotebook(preferredKernel, { uri: notebookUri, notebookType: 'interactive' }); } } @@ -428,6 +432,25 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'interactive.configure', + title: localize2('interactive.configExecute', 'Configure input box behavior'), + category: interactiveWindowCategory, + f1: false, + icon: icons.configIcon, + menu: { + id: MenuId.InteractiveInputConfig + } + }); + } + + override run(accessor: ServicesAccessor, ...args: any[]): void { + accessor.get(ICommandService).executeCommand('workbench.action.openSettings', '@tag:replExecute'); + } +}); + registerAction2(class extends Action2 { constructor() { super({ @@ -435,6 +458,11 @@ registerAction2(class extends Action2 { title: localize2('interactive.execute', 'Execute Code'), category: interactiveWindowCategory, keybinding: [{ + // when: NOTEBOOK_CELL_LIST_FOCUSED, + when: ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), + primary: KeyMod.CtrlCmd | KeyCode.Enter, + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT + }, { when: ContextKeyExpr.and( ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', true) @@ -448,18 +476,13 @@ registerAction2(class extends Action2 { ), primary: KeyCode.Enter, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT - }, { - // when: NOTEBOOK_CELL_LIST_FOCUSED, - when: ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), - primary: KeyMod.WinCtrl | KeyCode.Enter, - win: { - primary: KeyMod.CtrlCmd | KeyCode.Enter - }, - weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT }], menu: [ { id: MenuId.InteractiveInputExecute + }, + { + id: MenuId.ReplInputExecute } ], icon: icons.executeIcon, @@ -483,21 +506,29 @@ registerAction2(class extends Action2 { const historyService = accessor.get(IInteractiveHistoryService); const notebookEditorService = accessor.get(INotebookEditorService); let editorControl: { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; + let isReplEditor = false; if (context) { const resourceUri = URI.revive(context); - const editors = editorService.findEditors(resourceUri) - .filter(id => id.editor instanceof InteractiveEditorInput && id.editor.resource?.toString() === resourceUri.toString()); - if (editors.length) { - const editorInput = editors[0].editor as InteractiveEditorInput; - const currentGroup = editors[0].groupId; - const editor = await editorService.openEditor(editorInput, currentGroup); - editorControl = editor?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; + const editors = editorService.findEditors(resourceUri); + for (const found of editors) { + if (found.editor.typeId === ReplEditorInput.ID || found.editor.typeId === InteractiveEditorInput.ID) { + const editor = await editorService.openEditor(found.editor, found.groupId); + editorControl = editor?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; + isReplEditor = found.editor.typeId === ReplEditorInput.ID; + break; + } } } else { + const editor = editorService.activeEditorPane; + isReplEditor = editor instanceof ReplEditor; editorControl = editorService.activeEditorPane?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; } + if (editorControl && isReplEditor) { + executeReplInput(accessor, editorControl); + } + if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { const notebookDocument = editorControl.notebookEditor.textModel; const textModel = editorControl.codeEditor.getModel(); @@ -590,7 +621,7 @@ registerAction2(class extends Action2 { title: localize2('interactive.history.previous', 'Previous value in history'), category: interactiveWindowCategory, f1: false, - keybinding: { + keybinding: [{ when: ContextKeyExpr.and( ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('bottom'), @@ -599,7 +630,16 @@ registerAction2(class extends Action2 { ), primary: KeyCode.UpArrow, weight: KeybindingWeight.WorkbenchContrib - }, + }, { + when: ContextKeyExpr.and( + ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('bottom'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('none'), + SuggestContext.Visible.toNegated() + ), + primary: KeyCode.UpArrow, + weight: KeybindingWeight.WorkbenchContrib + }] }); } @@ -629,7 +669,7 @@ registerAction2(class extends Action2 { title: localize2('interactive.history.next', 'Next value in history'), category: interactiveWindowCategory, f1: false, - keybinding: { + keybinding: [{ when: ContextKeyExpr.and( ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('top'), @@ -638,7 +678,16 @@ registerAction2(class extends Action2 { ), primary: KeyCode.DownArrow, weight: KeybindingWeight.WorkbenchContrib - }, + }, { + when: ContextKeyExpr.and( + ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('top'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('none'), + SuggestContext.Visible.toNegated() + ), + primary: KeyCode.DownArrow, + weight: KeybindingWeight.WorkbenchContrib + }], }); } @@ -814,10 +863,17 @@ Registry.as(ConfigurationExtensions.Configuration).regis default: false, markdownDescription: localize('interactiveWindow.promptToSaveOnClose', "Prompt to save the interactive window when it is closed. Only new interactive windows will be affected by this setting change.") }, - ['interactiveWindow.executeWithShiftEnter']: { + [InteractiveWindowSetting.executeWithShiftEnter]: { + type: 'boolean', + default: false, + markdownDescription: localize('interactiveWindow.executeWithShiftEnter', "Execute the Interactive Window (REPL) input box with shift+enter, so that enter can be used to create a newline."), + tags: ['replExecute'] + }, + [InteractiveWindowSetting.showExecutionHint]: { type: 'boolean', default: true, - markdownDescription: localize('interactiveWindow.executeWithShiftEnter', "Execute the interactive window (REPL) input box with shift+enter, so that enter can be used to create a newline.") + markdownDescription: localize('interactiveWindow.showExecutionHint', "Display a hint in the Interactive Window (REPL) input box to indicate how to execute code."), + tags: ['replExecute'] } } }); diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts b/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts index 46a52d9b844..20ce01d8e34 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts @@ -8,5 +8,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const INTERACTIVE_INPUT_CURSOR_BOUNDARY = new RawContextKey<'none' | 'top' | 'bottom' | 'both'>('interactiveInputCursorAtBoundary', 'none'); export const InteractiveWindowSetting = { - interactiveWindowAlwaysScrollOnNewCell: 'interactiveWindow.alwaysScrollOnNewCell' + interactiveWindowAlwaysScrollOnNewCell: 'interactiveWindow.alwaysScrollOnNewCell', + executeWithShiftEnter: 'interactiveWindow.executeWithShiftEnter', + showExecutionHint: 'interactiveWindow.showExecutionHint' }; diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index ba994f540b6..a6b48582111 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -4,19 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/interactive'; -import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; -import { ICodeEditorViewState, IDecorationOptions } from 'vs/editor/common/editorCommon'; +import { ICodeEditorViewState } from 'vs/editor/common/editorCommon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorPaneSelectionChangeReason, IEditorMemento, IEditorOpenContext, IEditorPaneScrollPosition, IEditorPaneSelectionChangeEvent, IEditorPaneWithScrolling } from 'vs/workbench/common/editor'; @@ -63,6 +61,7 @@ import 'vs/css!./interactiveEditor'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { deepClone } from 'vs/base/common/objects'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ReplInputHintContentWidget } from 'vs/workbench/contrib/interactive/browser/replInputHintContentWidget'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; @@ -71,6 +70,7 @@ const INPUT_CELL_VERTICAL_PADDING = 8; const INPUT_CELL_HORIZONTAL_PADDING_RIGHT = 10; const INPUT_EDITOR_PADDING = 8; + export interface InteractiveEditorViewState { readonly notebook?: INotebookEditorViewState; readonly input?: ICodeEditorViewState | null; @@ -88,6 +88,7 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro private _inputCellContainer!: HTMLElement; private _inputFocusIndicator!: HTMLElement; private _inputRunButtonContainer!: HTMLElement; + private _inputConfigContainer!: HTMLElement; private _inputEditorContainer!: HTMLElement; private _codeEditorWidget!: CodeEditorWidget; private _notebookWidgetService: INotebookEditorService; @@ -109,6 +110,7 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro private _editorMemento: IEditorMemento; private readonly _groupListener = this._register(new MutableDisposable()); private _runbuttonToolbar: ToolBar | undefined; + private _hintElement: ReplInputHintContentWidget | undefined; private _onDidFocusWidget = this._register(new Emitter()); override get onDidFocus(): Event { return this._onDidFocusWidget.event; } @@ -163,11 +165,11 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._editorOptions = this._computeEditorOptions(); } })); - this._notebookOptions = new NotebookOptions(this.window, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); + this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); - this._register(this._keybindingService.onDidUpdateKeybindings(this._updateInputDecoration, this)); + this._register(this._keybindingService.onDidUpdateKeybindings(this._updateInputHint, this)); this._register(this._notebookExecutionStateService.onDidChangeExecution((e) => { if (e.type === NotebookExecutionType.cell && isEqual(e.notebook, this._notebookWidget.value?.viewModel?.notebookDocument.uri)) { const cell = this._notebookWidget.value?.getCellByHandle(e.cellHandle); @@ -197,9 +199,36 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._inputRunButtonContainer = DOM.append(this._inputCellContainer, DOM.$('.run-button-container')); this._setupRunButtonToolbar(this._inputRunButtonContainer); this._inputEditorContainer = DOM.append(this._inputCellContainer, DOM.$('.input-editor-container')); + this._setupConfigButtonToolbar(); this._createLayoutStyles(); } + private _setupConfigButtonToolbar() { + this._inputConfigContainer = DOM.append(this._inputEditorContainer, DOM.$('.input-toolbar-container')); + this._inputConfigContainer.style.position = 'absolute'; + this._inputConfigContainer.style.right = '0'; + this._inputConfigContainer.style.marginTop = '6px'; + this._inputConfigContainer.style.marginRight = '12px'; + this._inputConfigContainer.style.zIndex = '1'; + this._inputConfigContainer.style.display = 'none'; + + const menu = this._register(this._menuService.createMenu(MenuId.InteractiveInputConfig, this._contextKeyService)); + const toolbar = this._register(new ToolBar(this._inputConfigContainer, this._contextMenuService, { + getKeyBinding: action => this._keybindingService.lookupKeybinding(action.id), + actionViewItemProvider: (action, options) => { + return createActionViewItem(this._instantiationService, action, options); + }, + renderDropdownAsChildElement: true + })); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result); + toolbar.setActions([...primary, ...secondary]); + } + private _setupRunButtonToolbar(runButtonContainer: HTMLElement) { const menu = this._register(this._menuService.createMenu(MenuId.InteractiveInputExecute, this._contextKeyService)); this._runbuttonToolbar = this._register(new ToolBar(runButtonContainer, this._contextMenuService, { @@ -485,16 +514,26 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._widgetDisposableStore.add(this.themeService.onDidColorThemeChange(() => { if (this.isVisible()) { - this._updateInputDecoration(); + this._updateInputHint(); } })); this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModelContent(() => { if (this.isVisible()) { - this._updateInputDecoration(); + this._updateInputHint(); } })); + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModel(() => { + this._updateInputHint(); + })); + + this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(InteractiveWindowSetting.showExecutionHint)) { + this._updateInputHint(); + } + }); + const cursorAtBoundaryContext = INTERACTIVE_INPUT_CURSOR_BOUNDARY.bindTo(this._contextKeyService); if (input.resource && input.historyService.has(input.resource)) { cursorAtBoundaryContext.set('top'); @@ -535,6 +574,8 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._widgetDisposableStore.add(this._notebookWidget.value!.onDidScroll(() => this._onDidChangeScroll.fire())); this._syncWithKernel(); + + this._updateInputHint(); } override setOptions(options: INotebookEditorOptions | undefined): void { @@ -591,8 +632,6 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro NOTEBOOK_KERNEL.bindTo(this._contextKeyService).set(selectedOrSuggested.id); } } - - this._updateInputDecoration(); } layout(dimension: DOM.Dimension, position: DOM.IDomPosition): void { @@ -632,41 +671,24 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro return new DOM.Dimension(Math.max(0, width), Math.max(0, height)); } - private _updateInputDecoration(): void { + private _updateInputHint(): void { if (!this._codeEditorWidget) { return; } - if (!this._codeEditorWidget.hasModel()) { - return; + const shouldHide = + !this._codeEditorWidget.hasModel() || + this._configurationService.getValue(InteractiveWindowSetting.showExecutionHint) === false || + this._codeEditorWidget.getModel()!.getValueLength() !== 0; + + if (!this._hintElement && !shouldHide) { + this._hintElement = this._instantiationService.createInstance(ReplInputHintContentWidget, this._codeEditorWidget); + this._inputConfigContainer.style.display = 'block'; + } else if (this._hintElement && shouldHide) { + this._hintElement.dispose(); + this._hintElement = undefined; + this._inputConfigContainer.style.display = 'none'; } - - const model = this._codeEditorWidget.getModel(); - - const decorations: IDecorationOptions[] = []; - - if (model?.getValueLength() === 0) { - const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4); - const languageId = model.getLanguageId(); - const keybinding = this._keybindingService.lookupKeybinding('interactive.execute', this._contextKeyService)?.getLabel(); - const text = nls.localize('interactiveInputPlaceHolder', "Type '{0}' code here and press {1} to run", languageId, keybinding ?? 'ctrl+enter'); - decorations.push({ - range: { - startLineNumber: 0, - endLineNumber: 0, - startColumn: 0, - endColumn: 1 - }, - renderOptions: { - after: { - contentText: text, - color: transparentForeground ? transparentForeground.toString() : undefined - } - } - }); - } - - this._codeEditorWidget.setDecorationsByType('interactive-decoration', DECORATION_KEY, decorations); } getScrollPosition(): IEditorPaneScrollPosition { @@ -701,6 +723,8 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._notebookWidget.value.onWillHide(); } } + + this._updateInputHint(); } override clearInput() { diff --git a/src/vs/workbench/contrib/interactive/browser/replInputHintContentWidget.ts b/src/vs/workbench/contrib/interactive/browser/replInputHintContentWidget.ts new file mode 100644 index 00000000000..6dc4644b71a --- /dev/null +++ b/src/vs/workbench/contrib/interactive/browser/replInputHintContentWidget.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { status } from 'vs/base/browser/ui/aria/aria'; +import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; +import { Event } from 'vs/base/common/event'; +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { OS } from 'vs/base/common/platform'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { InteractiveWindowSetting } from 'vs/workbench/contrib/interactive/browser/interactiveCommon'; + + +export class ReplInputHintContentWidget extends Disposable implements IContentWidget { + + private static readonly ID = 'replInput.widget.emptyHint'; + + private domNode: HTMLElement | undefined; + private ariaLabel: string = ''; + + constructor( + private readonly editor: ICodeEditor, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + ) { + super(); + + this._register(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + if (this.domNode && e.hasChanged(EditorOption.fontInfo)) { + this.editor.applyFontInfo(this.domNode); + } + })); + const onDidFocusEditorText = Event.debounce(this.editor.onDidFocusEditorText, () => undefined, 500); + this._register(onDidFocusEditorText(() => { + if (this.editor.hasTextFocus() && this.ariaLabel && configurationService.getValue(AccessibilityVerbositySettingId.ReplInputHint)) { + status(this.ariaLabel); + } + })); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(InteractiveWindowSetting.executeWithShiftEnter)) { + this.setHint(); + } + })); + this.editor.addContentWidget(this); + } + + getId(): string { + return ReplInputHintContentWidget.ID; + } + + getPosition(): IContentWidgetPosition | null { + return { + position: { lineNumber: 1, column: 1 }, + preference: [ContentWidgetPositionPreference.EXACT] + }; + } + + getDomNode(): HTMLElement { + if (!this.domNode) { + this.domNode = dom.$('.empty-editor-hint'); + this.domNode.style.width = 'max-content'; + this.domNode.style.paddingLeft = '4px'; + + this.setHint(); + + this._register(dom.addDisposableListener(this.domNode, 'click', () => { + this.editor.focus(); + })); + + this.editor.applyFontInfo(this.domNode); + } + + return this.domNode; + } + + private setHint() { + if (!this.domNode) { + return; + } + while (this.domNode.firstChild) { + this.domNode.removeChild(this.domNode.firstChild); + } + + const hintElement = dom.$('div.empty-hint-text'); + hintElement.style.cursor = 'text'; + hintElement.style.whiteSpace = 'nowrap'; + + const keybinding = this.getKeybinding(); + const keybindingHintLabel = keybinding?.getLabel(); + + if (keybinding && keybindingHintLabel) { + const actionPart = localize('emptyHintText', 'Press {0} to execute. ', keybindingHintLabel); + + const [before, after] = actionPart.split(keybindingHintLabel).map((fragment) => { + const hintPart = dom.$('span', undefined, fragment); + hintPart.style.fontStyle = 'italic'; + return hintPart; + }); + + hintElement.appendChild(before); + + const label = new KeybindingLabel(hintElement, OS); + label.set(keybinding); + label.element.style.width = 'min-content'; + label.element.style.display = 'inline'; + + hintElement.appendChild(after); + this.domNode.append(hintElement); + + this.ariaLabel = actionPart.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.ReplInputHint)); + } + } + + private getKeybinding() { + const keybindings = this.keybindingService.lookupKeybindings('interactive.execute'); + const shiftEnterConfig = this.configurationService.getValue(InteractiveWindowSetting.executeWithShiftEnter); + const hasEnterChord = (kb: ResolvedKeybinding, modifier: string = '') => { + const chords = kb.getDispatchChords(); + const chord = modifier + 'Enter'; + const chordAlt = modifier + '[Enter]'; + return chords.length === 1 && (chords[0] === chord || chords[0] === chordAlt); + }; + + if (shiftEnterConfig) { + const keybinding = keybindings.find(kb => hasEnterChord(kb, 'shift+')); + if (keybinding) { + return keybinding; + } + } else { + let keybinding = keybindings.find(kb => hasEnterChord(kb)); + if (keybinding) { + return keybinding; + } + keybinding = this.keybindingService.lookupKeybindings('python.execInREPLEnter') + .find(kb => hasEnterChord(kb)); + if (keybinding) { + return keybinding; + } + } + + return keybindings?.[0]; + } + + override dispose(): void { + super.dispose(); + this.editor.removeContentWidget(this); + } +} diff --git a/src/vs/workbench/contrib/issue/browser/issue.contribution.ts b/src/vs/workbench/contrib/issue/browser/issue.contribution.ts index 28751d1c2c8..668418a0d46 100644 --- a/src/vs/workbench/contrib/issue/browser/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/browser/issue.contribution.ts @@ -10,21 +10,35 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { WebIssueService } from 'vs/workbench/services/issue/browser/issueService'; -import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { BrowserIssueService } from 'vs/workbench/contrib/issue/browser/issueService'; +import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; import { BaseIssueContribution } from 'vs/workbench/contrib/issue/common/issue.contribution'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IIssueMainService } from 'vs/platform/issue/common/issue'; +import { IssueFormService } from 'vs/workbench/contrib/issue/browser/issueFormService'; +import 'vs/workbench/contrib/issue/browser/issueTroubleshoot'; class WebIssueContribution extends BaseIssueContribution { constructor(@IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService) { super(productService, configurationService); + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + properties: { + 'issueReporter.experimental.webReporter': { + type: 'boolean', + default: productService.quality !== 'stable', + description: 'Enable experimental issue reporter for web.', + }, + } + }); } } Registry.as(Extensions.Workbench).registerWorkbenchContribution(WebIssueContribution, LifecyclePhase.Restored); -registerSingleton(IWorkbenchIssueService, WebIssueService, InstantiationType.Delayed); +registerSingleton(IWorkbenchIssueService, BrowserIssueService, InstantiationType.Delayed); +registerSingleton(IIssueMainService, IssueFormService, InstantiationType.Delayed); CommandsRegistry.registerCommand('_issues.getSystemStatus', (accessor) => { return nls.localize('statusUnsupported', "The --status argument is not yet supported in browsers."); diff --git a/src/vs/code/browser/issue/issue.ts b/src/vs/workbench/contrib/issue/browser/issue.ts similarity index 97% rename from src/vs/code/browser/issue/issue.ts rename to src/vs/workbench/contrib/issue/browser/issue.ts index 2205e847134..4f097ba4603 100644 --- a/src/vs/code/browser/issue/issue.ts +++ b/src/vs/workbench/contrib/issue/browser/issue.ts @@ -14,11 +14,12 @@ import { CancellationError } from 'vs/base/common/errors'; import { isLinuxSnap } from 'vs/base/common/platform'; import { escape } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/code/browser/issue/issueReporterModel'; import { localize } from 'vs/nls'; import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueType } from 'vs/platform/issue/common/issue'; import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; +import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; +import { mainWindow } from 'vs/base/browser/window'; const MAX_URL_LENGTH = 7500; @@ -156,7 +157,7 @@ export class BaseIssueReporterService extends Disposable { const content: string[] = []; if (styles.inputBackground) { - content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { background-color: ${styles.inputBackground}; }`); + content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { background-color: ${styles.inputBackground} !important; }`); } if (styles.inputBorder) { @@ -166,7 +167,7 @@ export class BaseIssueReporterService extends Disposable { } if (styles.inputForeground) { - content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { color: ${styles.inputForeground}; }`); + content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { color: ${styles.inputForeground} !important; }`); } if (styles.inputErrorBorder) { @@ -239,26 +240,6 @@ export class BaseIssueReporterService extends Disposable { } public setEventHandlers(): void { - this.addEventListener('issue-type', 'change', (event: Event) => { - const issueType = parseInt((event.target).value); - this.issueReporterModel.update({ issueType: issueType }); - if (issueType === IssueType.PerformanceIssue && !this.receivedPerformanceInfo) { - this.issueMainService.$getPerformanceInfo().then(info => { - this.updatePerformanceInfo(info as Partial); - }); - } - - // Resets placeholder - const descriptionTextArea = this.getElementById('issue-title'); - if (descriptionTextArea) { - descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); - } - - this.updatePreviewButtonState(); - this.setSourceOptions(); - this.render(); - }); - (['includeSystemInfo', 'includeProcessInfo', 'includeWorkspaceInfo', 'includeExtensions', 'includeExperiments', 'includeExtensionData'] as const).forEach(elementId => { this.addEventListener(elementId, 'click', (event: Event) => { event.stopPropagation(); @@ -335,6 +316,14 @@ export class BaseIssueReporterService extends Disposable { } }); + this.addEventListener('issue-title', 'input', _ => { + const titleElement = this.getElementById('issue-title') as HTMLInputElement; + if (titleElement) { + const title = titleElement.value; + this.issueReporterModel.update({ issueTitle: title }); + } + }); + this.addEventListener('issue-title', 'input', (e: Event) => { const title = (e.target).value; const lengthValidationMessage = this.getElementById('issue-title-length-validation-error'); @@ -781,7 +770,9 @@ export class BaseIssueReporterService extends Disposable { const inputElement = (this.getElementById(inputId)); const inputValidationMessage = this.getElementById(`${inputId}-empty-error`); const descriptionShortMessage = this.getElementById(`description-short-error`); - if (!inputElement.value) { + if (inputId === 'description' && this.nonGitHubIssueUrl && this.data.extensionId) { + return true; + } else if (!inputElement.value) { inputElement.classList.add('invalid-input'); inputValidationMessage?.classList.remove('hidden'); descriptionShortMessage?.classList.add('hidden'); @@ -791,8 +782,7 @@ export class BaseIssueReporterService extends Disposable { descriptionShortMessage?.classList.remove('hidden'); inputValidationMessage?.classList.add('hidden'); return false; - } - else { + } else { inputElement.classList.remove('invalid-input'); inputValidationMessage?.classList.add('hidden'); if (inputId === 'description') { @@ -825,7 +815,8 @@ export class BaseIssueReporterService extends Disposable { }), headers: new Headers({ 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.data.githubAccessToken}` + 'Authorization': `Bearer ${this.data.githubAccessToken}`, + 'User-Agent': 'request' }) }; @@ -835,8 +826,7 @@ export class BaseIssueReporterService extends Disposable { return false; } const result = await response.json(); - this.window.open(result.html_url, '_blank'); - + mainWindow.open(result.html_url, '_blank'); this.close(); return true; } @@ -1067,7 +1057,7 @@ export class BaseIssueReporterService extends Disposable { const showLoading = this.getElementById('ext-loading')!; show(showLoading); while (showLoading.firstChild) { - showLoading.removeChild(showLoading.firstChild); + showLoading.firstChild.remove(); } showLoading.append(element); @@ -1088,7 +1078,7 @@ export class BaseIssueReporterService extends Disposable { const hideLoading = this.getElementById('ext-loading')!; hide(hideLoading); if (hideLoading.firstChild) { - hideLoading.removeChild(element); + element.remove(); } this.renderBlocks(); } @@ -1193,5 +1183,3 @@ export function hide(el: Element | undefined | null) { export function show(el: Element | undefined | null) { el?.classList.remove('hidden'); } - - diff --git a/src/vs/workbench/contrib/issue/browser/issueFormService.ts b/src/vs/workbench/contrib/issue/browser/issueFormService.ts new file mode 100644 index 00000000000..1ac7d114454 --- /dev/null +++ b/src/vs/workbench/contrib/issue/browser/issueFormService.ts @@ -0,0 +1,221 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { safeInnerHtml } from 'vs/base/browser/dom'; +import { mainWindow } from 'vs/base/browser/window'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import BaseHtml from 'vs/workbench/contrib/issue/browser/issueReporterPage'; +import 'vs/css!./media/issueReporter'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { PerformanceInfo, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { ExtensionIdentifier, ExtensionIdentifierSet } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IIssueMainService, IssueReporterData, ProcessExplorerData } from 'vs/platform/issue/common/issue'; +import product from 'vs/platform/product/common/product'; +import { IssueWebReporter } from 'vs/workbench/contrib/issue/browser/issueReporterService'; +import { AuxiliaryWindowMode, IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; + +export interface IssuePassData { + issueTitle: string; + issueBody: string; +} + +export class IssueFormService implements IIssueMainService { + + readonly _serviceBrand: undefined; + + private issueReporterWindow: Window | null = null; + private extensionIdentifierSet: ExtensionIdentifierSet = new ExtensionIdentifierSet(); + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + + // listen for messages from the main window + mainWindow.addEventListener('message', async (event) => { + if (event.data && event.data.sendChannel === 'vscode:triggerReporterMenu') { + // gets menu actions from contributed + const actions = this.menuService.getMenuActions(MenuId.IssueReporter, this.contextKeyService, { renderShortTitle: true }).flatMap(entry => entry[1]); + + // render menu + for (const action of actions) { + try { + if (action.item && 'source' in action.item && action.item.source?.id === event.data.extensionId) { + this.extensionIdentifierSet.add(event.data.extensionId); + await action.run(); + } + } catch (error) { + console.error(error); + } + } + + if (!this.extensionIdentifierSet.has(event.data.extensionId)) { + // send undefined to indicate no action was taken + const replyChannel = `vscode:triggerReporterMenuResponse`; + mainWindow.postMessage({ replyChannel }, '*'); + } + } + }); + + } + + async openReporter(data: IssueReporterData): Promise { + if (data.extensionId && this.extensionIdentifierSet.has(data.extensionId)) { + const replyChannel = `vscode:triggerReporterMenuResponse`; + mainWindow.postMessage({ data, replyChannel }, '*'); + this.extensionIdentifierSet.delete(new ExtensionIdentifier(data.extensionId)); + } + + if (this.issueReporterWindow) { + const getModelData = await this.getIssueData(); + if (getModelData) { + const { issueTitle, issueBody } = getModelData; + if (issueTitle || issueBody) { + data.issueTitle = data.issueTitle ?? issueTitle; + data.issueBody = data.issueBody ?? issueBody; + + // close issue reporter and re-open with new data + this.issueReporterWindow.close(); + this.openAuxIssueReporter(data); + return; + } + } + this.issueReporterWindow.focus(); + return; + } + this.openAuxIssueReporter(data); + } + + async openAuxIssueReporter(data: IssueReporterData): Promise { + const disposables = new DisposableStore(); + + // Auxiliary Window + const auxiliaryWindow = disposables.add(await this.auxiliaryWindowService.open({ mode: AuxiliaryWindowMode.Normal })); + + this.issueReporterWindow = auxiliaryWindow.window; + + if (auxiliaryWindow) { + await auxiliaryWindow.whenStylesHaveLoaded; + auxiliaryWindow.window.document.title = 'Issue Reporter'; + auxiliaryWindow.window.document.body.classList.add('issue-reporter-body'); + + // custom issue reporter wrapper + const div = document.createElement('div'); + div.classList.add('monaco-workbench'); + + // removes preset monaco-workbench + auxiliaryWindow.container.remove(); + auxiliaryWindow.window.document.body.appendChild(div); + safeInnerHtml(div, BaseHtml()); + + // create issue reporter and instantiate + const issueReporter = this.instantiationService.createInstance(IssueWebReporter, false, data, { type: '', arch: '', release: '' }, product, auxiliaryWindow.window); + issueReporter.render(); + } else { + console.error('Failed to open auxiliary window'); + } + + // handle closing issue reporter + this.issueReporterWindow?.addEventListener('beforeunload', () => { + auxiliaryWindow.window.close(); + this.issueReporterWindow = null; + }); + } + + async openProcessExplorer(data: ProcessExplorerData): Promise { + throw new Error('Method not implemented.'); + } + + stopTracing(): Promise { + throw new Error('Method not implemented.'); + } + getSystemStatus(): Promise { + throw new Error('Method not implemented.'); + } + $getSystemInfo(): Promise { + throw new Error('Method not implemented.'); + } + $getPerformanceInfo(): Promise { + throw new Error('Method not implemented.'); + } + $reloadWithExtensionsDisabled(): Promise { + throw new Error('Method not implemented.'); + } + $showConfirmCloseDialog(): Promise { + throw new Error('Method not implemented.'); + } + $showClipboardDialog(): Promise { + throw new Error('Method not implemented.'); + } + $getIssueReporterUri(extensionId: string): Promise { + throw new Error('Method not implemented.'); + } + $getIssueReporterData(extensionId: string): Promise { + throw new Error('Method not implemented.'); + } + $getIssueReporterTemplate(extensionId: string): Promise { + throw new Error('Method not implemented.'); + } + $getReporterStatus(extensionId: string, extensionName: string): Promise { + throw new Error('Method not implemented.'); + } + + async $sendReporterMenu(extensionId: string, extensionName: string): Promise { + const sendChannel = `vscode:triggerReporterMenu`; + mainWindow.postMessage({ sendChannel, extensionId, extensionName }, '*'); + + const result = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + mainWindow.removeEventListener('message', listener); + reject(new Error('Timeout exceeded')); + }, 5000); // Set the timeout value in milliseconds (e.g., 5000 for 5 seconds) + + const listener = (event: MessageEvent) => { + const replyChannel = `vscode:triggerReporterMenuResponse`; + if (event.data && event.data.replyChannel === replyChannel) { + clearTimeout(timeout); + mainWindow.removeEventListener('message', listener); + resolve(event.data.data); + } + }; + mainWindow.addEventListener('message', listener); + }); + + return result as IssueReporterData | undefined; + } + + // Listens to data from the issue reporter model, which is updated regularly + async getIssueData(): Promise { + const sendChannel = `vscode:triggerIssueData`; + mainWindow.postMessage({ sendChannel }, '*'); + + const result = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + mainWindow.removeEventListener('message', listener); + reject(new Error('Timeout exceeded')); + }, 5000); // Set the timeout value in milliseconds (e.g., 5000 for 5 seconds) + + const listener = (event: MessageEvent) => { + const replyChannel = `vscode:triggerIssueDataResponse`; + if (event.data && event.data.replyChannel === replyChannel) { + clearTimeout(timeout); + mainWindow.removeEventListener('message', listener); + resolve(event.data.data); + } + }; + mainWindow.addEventListener('message', listener); + }); + + return result as IssuePassData | undefined; + } + + async $closeReporter(): Promise { + this.issueReporterWindow?.close(); + } +} diff --git a/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts b/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts index 37d52199b5b..a131f19d865 100644 --- a/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts +++ b/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts @@ -11,7 +11,7 @@ import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ThemeIcon } from 'vs/base/common/themables'; import { Codicon } from 'vs/base/common/codicons'; import { IssueSource } from 'vs/platform/issue/common/issue'; @@ -65,13 +65,8 @@ export class IssueQuickAccess extends PickerQuickAccessProvider entry[1]); - - menu.dispose(); + // gets menu actions from contributed + const actions = this.menuService.getMenuActions(MenuId.IssueReporter, this.contextKeyService, { renderShortTitle: true }).flatMap(entry => entry[1]); // create picks from contributed menu actions.forEach(action => { @@ -107,7 +102,7 @@ export class IssueQuickAccess extends PickerQuickAccessProvider { + if (event.data && event.data.sendChannel === 'vscode:triggerIssueData') { + mainWindow.postMessage({ + data: { issueBody: this._data.issueDescription, issueTitle: this._data.issueTitle }, + replyChannel: 'vscode:triggerIssueDataResponse' + }, '*'); + } + }); } getData(): IssueReporterData { @@ -138,6 +150,10 @@ ${this.getInfos()} if (this._data.includeSystemInfo && this._data.systemInfo) { info += this.generateSystemInfoMd(); } + + if (this._data.includeSystemInfo && this._data.systemInfoWeb) { + info += 'System Info: ' + this._data.systemInfoWeb; + } } if (this._data.issueType === IssueType.PerformanceIssue) { diff --git a/src/vs/code/browser/issue/issueReporterPage.ts b/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts similarity index 99% rename from src/vs/code/browser/issue/issueReporterPage.ts rename to src/vs/workbench/contrib/issue/browser/issueReporterPage.ts index 7cf33372a97..195857e0d4b 100644 --- a/src/vs/code/browser/issue/issueReporterPage.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts @@ -24,7 +24,7 @@ const reviewGuidanceLabel = localize( // intentionally not escaped because of it ); export default (): string => ` -
+
${reviewGuidanceLabel}
diff --git a/src/vs/code/browser/issue/issueReporterService.ts b/src/vs/workbench/contrib/issue/browser/issueReporterService.ts similarity index 73% rename from src/vs/code/browser/issue/issueReporterService.ts rename to src/vs/workbench/contrib/issue/browser/issueReporterService.ts index d724e028e94..1c7b878ce39 100644 --- a/src/vs/code/browser/issue/issueReporterService.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterService.ts @@ -2,13 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { $, isHTMLInputElement, isHTMLTextAreaElement, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { Codicon } from 'vs/base/common/codicons'; import { groupBy } from 'vs/base/common/collections'; import { isMacintosh } from 'vs/base/common/platform'; import { IProductConfiguration } from 'vs/base/common/product'; -import { BaseIssueReporterService } from 'vs/code/browser/issue/issue'; +import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; import { IIssueMainService, IssueReporterData, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; +import { BaseIssueReporterService } from 'vs/workbench/contrib/issue/browser/issue'; // GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe. // ref https://github.com/microsoft/vscode/issues/159191 @@ -27,6 +29,17 @@ export class IssueWebReporter extends BaseIssueReporterService { @IIssueMainService issueMainService: IIssueMainService ) { super(disableExtensions, data, os, product, window, true, issueMainService); + + const target = this.window.document.querySelector('.block-system .block-info'); + + const webInfo = this.window.navigator.userAgent; + if (webInfo) { + target?.appendChild(this.window.document.createTextNode(webInfo)); + this.receivedSystemInfo = true; + this.issueReporterModel.update({ systemInfoWeb: webInfo }); + + } + this.setEventHandlers(); this.handleExtensionData(data.enabledExtensions); } @@ -59,6 +72,20 @@ export class IssueWebReporter extends BaseIssueReporterService { public override setEventHandlers(): void { super.setEventHandlers(); + this.addEventListener('issue-type', 'change', (event: Event) => { + const issueType = parseInt((event.target).value); + this.issueReporterModel.update({ issueType: issueType }); + + // Resets placeholder + const descriptionTextArea = this.getElementById('issue-title'); + if (descriptionTextArea) { + descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); + } + + this.updatePreviewButtonState(); + this.setSourceOptions(); + this.render(); + }); this.previewButton.onDidClick(async () => { this.delayedSubmit.trigger(async () => { this.createIssue(); @@ -111,7 +138,7 @@ export class IssueWebReporter extends BaseIssueReporterService { // Manually perform the selection if (isMacintosh) { if (cmdOrCtrlKey && e.key === 'a' && e.target) { - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + if (isHTMLInputElement(e.target) || isHTMLTextAreaElement(e.target)) { (e.target).select(); } } @@ -174,7 +201,31 @@ export class IssueWebReporter extends BaseIssueReporterService { this.issueReporterModel.update({ selectedExtension: matches[0] }); const selectedExtension = this.issueReporterModel.getData().selectedExtension; if (selectedExtension) { - await this.sendReporterMenu(selectedExtension); + const iconElement = document.createElement('span'); + iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); + this.setLoading(iconElement); + const openReporterData = await this.sendReporterMenu(selectedExtension); + if (openReporterData) { + if (this.selectedExtension === selectedExtensionId) { + this.removeLoading(iconElement, true); + this.data = openReporterData; + } else if (this.selectedExtension !== selectedExtensionId) { + } + } + else { + if (!this.loadingExtensionData) { + iconElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); + } + this.removeLoading(iconElement); + this.clearExtensionData(); + selectedExtension.data = undefined; + selectedExtension.uri = undefined; + } + if (this.selectedExtension === selectedExtensionId) { + // repopulates the fields with the new data given the selected extension. + this.updateExtensionStatus(matches[0]); + this.openReporter = false; + } } else { this.issueReporterModel.update({ selectedExtension: undefined }); this.clearSearchResults(); diff --git a/src/vs/workbench/contrib/issue/browser/issueService.ts b/src/vs/workbench/contrib/issue/browser/issueService.ts new file mode 100644 index 00000000000..6030bbfd928 --- /dev/null +++ b/src/vs/workbench/contrib/issue/browser/issueService.ts @@ -0,0 +1,211 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { userAgent } from 'vs/base/common/platform'; +import { IExtensionDescription, ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; +import { getZoomLevel } from 'vs/base/browser/browser'; +import { mainWindow } from 'vs/base/browser/window'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles } from 'vs/platform/issue/common/issue'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { buttonBackground, buttonForeground, buttonHoverBackground, foreground, inputActiveOptionBorder, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; +import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + + +export class BrowserIssueService implements IWorkbenchIssueService { + declare readonly _serviceBrand: undefined; + + constructor( + @IExtensionService private readonly extensionService: IExtensionService, + @IProductService private readonly productService: IProductService, + @IIssueMainService private readonly issueMainService: IIssueMainService, + @IThemeService private readonly themeService: IThemeService, + @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IIntegrityService private readonly integrityService: IIntegrityService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { } + + //TODO @TylerLeonhardt @Tyriar to implement a process explorer for the web + async openProcessExplorer(): Promise { + console.error('openProcessExplorer is not implemented in web'); + } + + async openReporter(options: Partial): Promise { + // If web reporter setting is false open the old GitHub issue reporter + if (!this.configurationService.getValue('issueReporter.experimental.webReporter')) { + const extensionId = options.extensionId; + // If we don't have a extensionId, treat this as a Core issue + if (!extensionId) { + if (this.productService.reportIssueUrl) { + const uri = this.getIssueUriFromStaticContent(this.productService.reportIssueUrl); + dom.windowOpenNoOpener(uri); + return; + } + throw new Error(`No issue reporting URL configured for ${this.productService.nameLong}.`); + } + + const selectedExtension = this.extensionService.extensions.filter(ext => ext.identifier.value === options.extensionId)[0]; + const extensionGitHubUrl = this.getExtensionGitHubUrl(selectedExtension); + if (!extensionGitHubUrl) { + throw new Error(`Unable to find issue reporting url for ${extensionId}`); + } + const uri = this.getIssueUriFromStaticContent(`${extensionGitHubUrl}/issues/new`, selectedExtension); + dom.windowOpenNoOpener(uri); + } + + if (this.productService.reportIssueUrl) { + const theme = this.themeService.getColorTheme(); + const experiments = await this.experimentService.getCurrentExperiments(); + + let githubAccessToken = ''; + try { + const githubSessions = await this.authenticationService.getSessions('github'); + const potentialSessions = githubSessions.filter(session => session.scopes.includes('repo')); + githubAccessToken = potentialSessions[0]?.accessToken; + } catch (e) { + // Ignore + } + + // air on the side of caution and have false be the default + let isUnsupported = false; + try { + isUnsupported = !(await this.integrityService.isPure()).isPure; + } catch (e) { + // Ignore + } + + const extensionData: IssueReporterExtensionData[] = []; + try { + const extensions = await this.extensionManagementService.getInstalled(); + const enabledExtensions = extensions.filter(extension => this.extensionEnablementService.isEnabled(extension) || (options.extensionId && extension.identifier.id === options.extensionId)); + extensionData.push(...enabledExtensions.map((extension): IssueReporterExtensionData => { + const { manifest } = extension; + const manifestKeys = manifest.contributes ? Object.keys(manifest.contributes) : []; + const isTheme = !manifest.main && !manifest.browser && manifestKeys.length === 1 && manifestKeys[0] === 'themes'; + const isBuiltin = extension.type === ExtensionType.System; + return { + name: manifest.name, + publisher: manifest.publisher, + version: manifest.version, + repositoryUrl: manifest.repository && manifest.repository.url, + bugsUrl: manifest.bugs && manifest.bugs.url, + displayName: manifest.displayName, + id: extension.identifier.id, + data: options.data, + uri: options.uri, + isTheme, + isBuiltin, + extensionData: 'Extensions data loading', + }; + })); + } catch (e) { + extensionData.push({ + name: 'Workbench Issue Service', + publisher: 'Unknown', + version: 'Unknown', + repositoryUrl: undefined, + bugsUrl: undefined, + extensionData: `Extensions not loaded: ${e}`, + displayName: `Extensions not loaded: ${e}`, + id: 'workbench.issue', + isTheme: false, + isBuiltin: true + }); + } + + const issueReporterData: IssueReporterData = Object.assign({ + styles: getIssueReporterStyles(theme), + zoomLevel: getZoomLevel(mainWindow), + enabledExtensions: extensionData, + experiments: experiments?.join('\n'), + restrictedMode: !this.workspaceTrustManagementService.isWorkspaceTrusted(), + isUnsupported, + githubAccessToken + }, options); + + return this.issueMainService.openReporter(issueReporterData); + } + throw new Error(`No issue reporting URL configured for ${this.productService.nameLong}.`); + + } + + private getExtensionGitHubUrl(extension: IExtensionDescription): string { + if (extension.isBuiltin && this.productService.reportIssueUrl) { + return normalizeGitHubUrl(this.productService.reportIssueUrl); + } + + let repositoryUrl = ''; + + const bugsUrl = extension?.bugs?.url; + const extensionUrl = extension?.repository?.url; + + // If given, try to match the extension's bug url + if (bugsUrl && bugsUrl.match(/^https?:\/\/github\.com\/(.*)/)) { + repositoryUrl = normalizeGitHubUrl(bugsUrl); + } else if (extensionUrl && extensionUrl.match(/^https?:\/\/github\.com\/(.*)/)) { + repositoryUrl = normalizeGitHubUrl(extensionUrl); + } + + return repositoryUrl; + } + + private getIssueUriFromStaticContent(baseUri: string, extension?: IExtensionDescription): string { + const issueDescription = `ADD ISSUE DESCRIPTION HERE + +Version: ${this.productService.version} +Commit: ${this.productService.commit ?? 'unknown'} +User Agent: ${userAgent ?? 'unknown'} +Embedder: ${this.productService.embedderIdentifier ?? 'unknown'} +${extension?.version ? `\nExtension version: ${extension.version}` : ''} +`; + + return `${baseUri}?body=${encodeURIComponent(issueDescription)}&labels=web`; + } +} + +export function getIssueReporterStyles(theme: IColorTheme): IssueReporterStyles { + return { + backgroundColor: getColor(theme, SIDE_BAR_BACKGROUND), + color: getColor(theme, foreground), + textLinkColor: getColor(theme, textLinkForeground), + textLinkActiveForeground: getColor(theme, textLinkActiveForeground), + inputBackground: getColor(theme, inputBackground), + inputForeground: getColor(theme, inputForeground), + inputBorder: getColor(theme, inputBorder), + inputActiveBorder: getColor(theme, inputActiveOptionBorder), + inputErrorBorder: getColor(theme, inputValidationErrorBorder), + inputErrorBackground: getColor(theme, inputValidationErrorBackground), + inputErrorForeground: getColor(theme, inputValidationErrorForeground), + buttonBackground: getColor(theme, buttonBackground), + buttonForeground: getColor(theme, buttonForeground), + buttonHoverBackground: getColor(theme, buttonHoverBackground), + sliderActiveColor: getColor(theme, scrollbarSliderActiveBackground), + sliderBackgroundColor: getColor(theme, scrollbarSliderBackground), + sliderHoverColor: getColor(theme, scrollbarSliderHoverBackground), + }; +} + +function getColor(theme: IColorTheme, key: string): string | undefined { + const color = theme.getColor(key); + return color ? color.toString() : undefined; +} + +registerSingleton(IWorkbenchIssueService, BrowserIssueService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/issue/browser/issueTroubleshoot.ts b/src/vs/workbench/contrib/issue/browser/issueTroubleshoot.ts similarity index 99% rename from src/vs/workbench/services/issue/browser/issueTroubleshoot.ts rename to src/vs/workbench/contrib/issue/browser/issueTroubleshoot.ts index 86f84f2af22..f9f3104f8b3 100644 --- a/src/vs/workbench/services/issue/browser/issueTroubleshoot.ts +++ b/src/vs/workbench/contrib/issue/browser/issueTroubleshoot.ts @@ -7,7 +7,7 @@ import { localize, localize2 } from 'vs/nls'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IUserDataProfileImportExportService, IUserDataProfileManagementService, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; diff --git a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css new file mode 100644 index 00000000000..2f2bf7d3e6b --- /dev/null +++ b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css @@ -0,0 +1,458 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.web.issue-reporter-body { + position: absolute; + overflow-y: scroll; +} + +.web.issue-reporter-body .monaco-workbench select{ + -webkit-appearance: auto; + appearance: auto; +} + +/** + * Table + */ + +.issue-reporter table { + width: 100%; + max-width: 100%; + background-color: transparent; + border-collapse: collapse; +} + +.issue-reporter th { + vertical-align: bottom; + border-bottom: 1px solid; + padding: 5px; + text-align: inherit; +} + +.issue-reporter td { + padding: 5px; + vertical-align: top; +} + +.issue-reporter tr td:first-child { + width: 30%; +} + +.issue-reporter label { + user-select: none; +} + +.issue-reporter .block-settingsSearchResults-details { + padding-bottom: .5rem; +} + +.issue-reporter .block-settingsSearchResults-details > div { + padding: .5rem .75rem; +} + +.issue-reporter .section { + margin-bottom: .5em; +} + +/** + * Forms + */ +.issue-reporter input[type="text"], +.issue-reporter textarea { + display: block; + width: 100%; + padding: .375rem .75rem; + font-size: 1rem; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: 1px solid #ced4da; +} + +.issue-reporter textarea { + overflow: auto; + resize: vertical; +} + +/** + * Button + */ + +.issue-reporter .monaco-text-button { + display: block; + width: auto; + padding: 4px 10px; + align-self: flex-end; + margin-bottom: 1em; + font-size: 13px; +} + +.issue-reporter select { + height: calc(2.25rem + 2px); + display: inline-block; + padding: 3px 3px; + font-size: 14px; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: none; +} + +.issue-reporter * { + box-sizing: border-box; +} + +.issue-reporter textarea, +.issue-reporter input, +.issue-reporter select { + font-family: inherit; +} + +.issue-reporter html { + color: #CCCCCC; + height: 100%; +} + +.issue-reporter .extension-caption .codicon-modifier-spin { + padding-bottom: 3px; + margin-left: 2px; +} + +/* Font Families (with CJK support) */ + +.issue-reporter .mac.web { + font-family: -apple-system, BlinkMacSystemFont, sans-serif; +} + +.issue-reporter .mac.web:lang(zh-Hans) { + font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; +} + +.issue-reporter .mac.web:lang(zh-Hant) { + font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; +} + +.issue-reporter .mac.web:lang(ja) { + font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; +} + +.issue-reporter .mac.web:lang(ko) { + font-family: -apple-system, BlinkMacSystemFont, "Nanum Gothic", "Apple SD Gothic Neo", "AppleGothic", sans-serif; +} + +.issue-reporter .windows.web { + font-family: "Segoe WPC", "Segoe UI", sans-serif; +} + +.issue-reporter .windows.web:lang(zh-Hans) { + font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; +} + +.issue-reporter .windows.web:lang(zh-Hant) { + font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; +} + +.issue-reporter .windows.web:lang(ja) { + font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; +} + +.issue-reporter .windows.web:lang(ko) { + font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; +} + +.issue-reporter .linux.web { + font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; +} + +.issue-reporter .linux.web:lang(zh-Hans) { + font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; +} + +.issue-reporter .linux.web:lang(zh-Hant) { + font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; +} + +.issue-reporter .linux.web:lang(ja) { + font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; +} + +.issue-reporter .linux.web:lang(ko) { + font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; +} + +.issue-reporter body { + margin: 0; + overflow-y: scroll; + height: 100%; +} + +.issue-reporter .hidden { + display: none; +} + +.issue-reporter .block { + font-size: 12px; +} + +.issue-reporter .block .block-info { + width: 100%; + font-size: 12px; + overflow: auto; + overflow-wrap: break-word; + margin: 5px; + padding: 10px; +} + +.issue-reporter { + max-width: 85vw; + margin-left: auto; + margin-right: auto; + padding-top: 2em; + padding-bottom: 2em; + display: flex; + flex-direction: column; + min-height: 100%; + overflow: visible; +} + +.issue-reporter .description-section { + flex-grow: 1; + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.issue-reporter textarea { + flex-grow: 1; + min-height: 150px; +} + +.issue-reporter .block-info-text { + display: flex; + flex-grow: 1; +} + +.issue-reporter #github-submit-btn { + flex-shrink: 0; + margin-left: auto; + margin-top: 10px; + margin-bottom: 10px; +} + +.issue-reporter .two-col { + display: inline-block; + width: 49%; +} + +.issue-reporter #vscode-version { + width: 90%; +} + +.issue-reporter .input-group { + margin-bottom: 1em; + font-size: 16px; +} + +.issue-reporter #extension-selection { + margin-top: 1em; +} + +.issue-reporter select, +.issue-reporter input, +.issue-reporter textarea { + border: 1px solid transparent; + margin-top: 10px; +} + + +.issue-reporter .validation-error { + font-size: 12px; + padding: 10px; + border-top: 0px !important; +} + +.issue-reporter .system-info { + margin-bottom: 10px; +} + + +.issue-reporter input[type="checkbox"] { + width: auto; + display: inline-block; + margin-top: 0; + vertical-align: middle; + cursor: pointer; +} + +.issue-reporter input:disabled { + opacity: 0.6; +} + +.issue-reporter .list-title { + margin-top: 1em; + margin-left: 1em; +} + +.issue-reporter .instructions { + font-size: 12px; + margin-top: .5em; +} + +.issue-reporter a, +.issue-reporter .workbenchCommand { + cursor: pointer; + border: 1px solid transparent; +} + +.issue-reporter .workbenchCommand:disabled { + color: #868e96; + cursor: default +} + +.issue-reporter .block-extensions .block-info { + margin-bottom: 1.5em; +} + +/* Default styles, overwritten if a theme is provided */ +.issue-reporter input, +.issue-reporter select, +.issue-reporter textarea { + background-color: #3c3c3c; + border: none; + color: #cccccc; +} + +.issue-reporter .section .input-group .validation-error { + margin-left: 100px; +} + +.issue-reporter .section .inline-form-control, +.issue-reporter .section .inline-label { + display: inline-block; +} + +.issue-reporter .section .inline-label { + width: 95px; +} + +.issue-reporter .section .inline-form-control, +.issue-reporter .section .input-group .validation-error { + width: calc(100% - 100px); +} + +.issue-reporter #issue-type { + cursor: pointer; +} + +.issue-reporter #similar-issues { + margin-left: 15%; + display: block; +} + +.issue-reporter #problem-source-help-text { + margin-left: calc(15% + 1em); +} + +@media (max-width: 950px) { + .issue-reporter .section .inline-label { + width: 15%; + font-size: 16px; + } + + .issue-reporter #problem-source-help-text { + margin-left: calc(15% + 1em); + } + + .issue-reporter .section .inline-form-control, + .issue-reporter .section .input-group .validation-error { + width: calc(85% - 5px); + } + + .issue-reporter .section .input-group .validation-error { + margin-left: calc(15% + 4px); + } +} + +@media (max-width: 620px) { + .issue-reporter .section .inline-label { + display: none !important; + } + + .issue-reporter #problem-source-help-text { + margin-left: 1em; + } + + .issue-reporter .section .inline-form-control, + .issue-reporter .section .input-group .validation-error { + width: 100%; + } + + .issue-reporter #similar-issues, + .issue-reporter .section .input-group .validation-error { + margin-left: 0; + } +} + +.issue-reporter ::-webkit-scrollbar { + width: 14px; +} + +.issue-reporter ::-webkit-scrollbar-thumb { + min-height: 20px; +} + +.issue-reporter ::-webkit-scrollbar-corner { + display: none; +} + +.issue-reporter .issues-container { + margin-left: 1.5em; + margin-top: .5em; + max-height: 92px; + overflow-y: auto; +} + +.issue-reporter .issues-container > .issue { + padding: 4px 0; + display: flex; +} + +.issue-reporter .issues-container > .issue > .issue-link { + width: calc(100% - 82px); + overflow: hidden; + padding-top: 3px; + white-space: nowrap; + text-overflow: ellipsis; +} + +.issue-reporter .issues-container > .issue > .issue-state .codicon { + width: 16px; +} + +.issue-reporter .issues-container > .issue > .issue-state { + display: flex; + width: 77px; + padding: 3px 6px; + margin-right: 5px; + color: #CCCCCC; + background-color: #3c3c3c; + border-radius: .25rem; +} + +.issue-reporter .issues-container > .issue .label { + padding-top: 2px; + margin-left: 5px; + width: 44px; + text-overflow: ellipsis; + overflow: hidden; +} + +.issue-reporter .issues-container > .issue .issue-icon { + padding-top: 2px; +} + +.issue-reporter a { + color: var(--vscode-textLink-foreground); +} diff --git a/src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts b/src/vs/workbench/contrib/issue/browser/test/testReporterModel.test.ts similarity index 97% rename from src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts rename to src/vs/workbench/contrib/issue/browser/test/testReporterModel.test.ts index 1708f3a07a1..f90b10bade8 100644 --- a/src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts +++ b/src/vs/workbench/contrib/issue/browser/test/testReporterModel.test.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +// eslint-disable-next-line local/code-import-patterns +import assert from 'assert'; +// eslint-disable-next-line local/code-import-patterns import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { IssueReporterModel } from 'vs/code/browser/issue/issueReporterModel'; +import { IssueReporterModel } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; import { IssueType } from 'vs/platform/issue/common/issue'; import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; diff --git a/src/vs/workbench/contrib/issue/common/issue.contribution.ts b/src/vs/workbench/contrib/issue/common/issue.contribution.ts index 9e6ee979ac9..8e4d7795710 100644 --- a/src/vs/workbench/contrib/issue/common/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/common/issue.contribution.ts @@ -11,7 +11,7 @@ import { CommandsRegistry, ICommandMetadata } from 'vs/platform/commands/common/ import { IssueReporterData } from 'vs/platform/issue/common/issue'; import { IProductService } from 'vs/platform/product/common/productService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/services/issue/common/issue.ts b/src/vs/workbench/contrib/issue/common/issue.ts similarity index 80% rename from src/vs/workbench/services/issue/common/issue.ts rename to src/vs/workbench/contrib/issue/common/issue.ts index 3ecd103cf58..5834ba051be 100644 --- a/src/vs/workbench/services/issue/common/issue.ts +++ b/src/vs/workbench/contrib/issue/common/issue.ts @@ -10,5 +10,12 @@ export const IWorkbenchIssueService = createDecorator('w export interface IWorkbenchIssueService { readonly _serviceBrand: undefined; openReporter(dataOverrides?: Partial): Promise; +} + +export const IWorkbenchProcessService = createDecorator('workbenchProcessService'); + +export interface IWorkbenchProcessService { + readonly _serviceBrand: undefined; openProcessExplorer(): Promise; } + diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts index d6c76d7fbd9..9eacc2f7574 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { localize, localize2 } from 'vs/nls'; -import { MenuRegistry, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; -import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; import { BaseIssueContribution } from 'vs/workbench/contrib/issue/common/issue.contribution'; import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -14,16 +13,14 @@ import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { INativeHostService } from 'vs/platform/native/common/native'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { IIssueMainService, IssueType } from 'vs/platform/issue/common/issue'; +import { IssueType } from 'vs/platform/issue/common/issue'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { IssueQuickAccess } from 'vs/workbench/contrib/issue/browser/issueQuickAccess'; - +import 'vs/workbench/contrib/issue/electron-sandbox/issueMainService'; +import 'vs/workbench/contrib/issue/electron-sandbox/issueService'; +import 'vs/workbench/contrib/issue/browser/issueTroubleshoot'; //#region Issue Contribution @@ -84,87 +81,10 @@ class ReportPerformanceIssueUsingReporterAction extends Action2 { } override async run(accessor: ServicesAccessor): Promise { - const issueService = accessor.get(IWorkbenchIssueService); + const issueService = accessor.get(IWorkbenchIssueService); // later can just get IIssueFormService return issueService.openReporter({ issueType: IssueType.PerformanceIssue }); } } -//#endregion - -//#region Commands - -class OpenProcessExplorer extends Action2 { - - static readonly ID = 'workbench.action.openProcessExplorer'; - - constructor() { - super({ - id: OpenProcessExplorer.ID, - title: localize2('openProcessExplorer', 'Open Process Explorer'), - category: Categories.Developer, - f1: true - }); - } - - override async run(accessor: ServicesAccessor): Promise { - const issueService = accessor.get(IWorkbenchIssueService); - - return issueService.openProcessExplorer(); - } -} -registerAction2(OpenProcessExplorer); -MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { - group: '5_tools', - command: { - id: OpenProcessExplorer.ID, - title: localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer") - }, - order: 2 -}); - -class StopTracing extends Action2 { - - static readonly ID = 'workbench.action.stopTracing'; - - constructor() { - super({ - id: StopTracing.ID, - title: localize2('stopTracing', 'Stop Tracing'), - category: Categories.Developer, - f1: true - }); - } - - override async run(accessor: ServicesAccessor): Promise { - const issueService = accessor.get(IIssueMainService); - const environmentService = accessor.get(INativeEnvironmentService); - const dialogService = accessor.get(IDialogService); - const nativeHostService = accessor.get(INativeHostService); - const progressService = accessor.get(IProgressService); - - if (!environmentService.args.trace) { - const { confirmed } = await dialogService.confirm({ - message: localize('stopTracing.message', "Tracing requires to launch with a '--trace' argument"), - primaryButton: localize({ key: 'stopTracing.button', comment: ['&& denotes a mnemonic'] }, "&&Relaunch and Enable Tracing"), - }); - - if (confirmed) { - return nativeHostService.relaunch({ addArgs: ['--trace'] }); - } - } - - await progressService.withProgress({ - location: ProgressLocation.Dialog, - title: localize('stopTracing.title', "Creating trace file..."), - cancellable: false, - detail: localize('stopTracing.detail', "This can take up to one minute to complete.") - }, () => issueService.stopTracing()); - } -} -registerAction2(StopTracing); - -CommandsRegistry.registerCommand('_issues.getSystemStatus', (accessor) => { - return accessor.get(IIssueMainService).getSystemStatus(); -}); -//#endregion +// #endregion diff --git a/src/vs/workbench/services/issue/electron-sandbox/issueMainService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts similarity index 76% rename from src/vs/workbench/services/issue/electron-sandbox/issueMainService.ts rename to src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts index a3cb28473af..cf16313519a 100644 --- a/src/vs/workbench/services/issue/electron-sandbox/issueMainService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; -import { IIssueMainService } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IProcessMainService } from 'vs/platform/issue/common/issue'; registerMainProcessRemoteService(IIssueMainService, 'issue'); +registerMainProcessRemoteService(IProcessMainService, 'process'); + diff --git a/src/vs/code/electron-sandbox/issue/issueReporter-dev.html b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter-dev.html similarity index 79% rename from src/vs/code/electron-sandbox/issue/issueReporter-dev.html rename to src/vs/workbench/contrib/issue/electron-sandbox/issueReporter-dev.html index a303a97d475..455e823692a 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporter-dev.html +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter-dev.html @@ -39,8 +39,8 @@ - - - + + + diff --git a/src/vs/code/electron-sandbox/issue/issueReporter.html b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.html similarity index 100% rename from src/vs/code/electron-sandbox/issue/issueReporter.html rename to src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.html diff --git a/src/vs/code/electron-sandbox/issue/issueReporter.js b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js similarity index 78% rename from src/vs/code/electron-sandbox/issue/issueReporter.js rename to src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js index b486c703057..aad5671f1f0 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporter.js +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js @@ -7,10 +7,14 @@ (function () { 'use strict'; + /** + * @import { ISandboxConfiguration } from '../../../../base/parts/sandbox/common/sandboxTypes' + */ + const bootstrapWindow = bootstrapWindowLib(); // Load issue reporter into window - bootstrapWindow.load(['vs/code/electron-sandbox/issue/issueReporterMain'], function (issueReporter, configuration) { + bootstrapWindow.load(['vs/workbench/contrib/issue/electron-sandbox/issueReporterMain'], function (issueReporter, configuration) { return issueReporter.startup(configuration); }, { @@ -24,12 +28,10 @@ ); /** - * @typedef {import('../../../base/parts/sandbox/common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration - * * @returns {{ * load: ( * modules: string[], - * resultCallback: (result, configuration: ISandboxConfiguration) => unknown, + * resultCallback: (result: any, configuration: ISandboxConfiguration) => unknown, * options?: { * configureDeveloperSettings?: (config: ISandboxConfiguration) => { * forceEnableDeveloperKeybindings?: boolean, diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts similarity index 87% rename from src/vs/code/electron-sandbox/issue/issueReporterMain.ts rename to src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts index c72367bff1a..ca9253bb1e9 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts @@ -6,7 +6,7 @@ import { safeInnerHtml } from 'vs/base/browser/dom'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded import { isLinux, isWindows } from 'vs/base/common/platform'; -import BaseHtml from 'vs/code/browser/issue/issueReporterPage'; +import BaseHtml from 'vs/workbench/contrib/issue/browser/issueReporterPage'; import 'vs/css!./media/issueReporter'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; @@ -15,12 +15,13 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; -import { IIssueMainService, IssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IProcessMainService, IssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; import { INativeHostService } from 'vs/platform/native/common/native'; import { NativeHostService } from 'vs/platform/native/common/nativeHostService'; -import { IssueReporter2 } from 'vs/code/electron-sandbox/issue/issueReporterService2'; +import { IssueReporter2 } from 'vs/workbench/contrib/issue/electron-sandbox/issueReporterService2'; import { mainWindow } from 'vs/base/browser/window'; + export function startup(configuration: IssueReporterWindowConfiguration) { const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac'; mainWindow.document.body.classList.add(platformClass); // used by our fonts @@ -50,3 +51,4 @@ function initServices(windowId: number) { } registerMainProcessRemoteService(IIssueMainService, 'issue'); +registerMainProcessRemoteService(IProcessMainService, 'process'); diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts similarity index 98% rename from src/vs/code/electron-sandbox/issue/issueReporterService.ts rename to src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts index a295a881487..4fdf80be67d 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, createStyleSheet, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { $, createStyleSheet, isHTMLInputElement, isHTMLTextAreaElement, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; import { Button, unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { mainWindow } from 'vs/base/browser/window'; @@ -16,10 +16,10 @@ import { isLinuxSnap, isMacintosh } from 'vs/base/common/platform'; import { escape } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/code/browser/issue/issueReporterModel'; +import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; -import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IProcessMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; import { INativeHostService } from 'vs/platform/native/common/native'; import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; @@ -59,7 +59,8 @@ export class IssueReporter extends Disposable { constructor( private readonly configuration: IssueReporterWindowConfiguration, @INativeHostService private readonly nativeHostService: INativeHostService, - @IIssueMainService private readonly issueMainService: IIssueMainService + @IIssueMainService private readonly issueMainService: IIssueMainService, + @IProcessMainService private readonly processMainService: IProcessMainService ) { super(); const targetExtension = configuration.data.extensionId ? configuration.data.enabledExtensions.find(extension => extension.id.toLocaleLowerCase() === configuration.data.extensionId?.toLocaleLowerCase()) : undefined; @@ -107,7 +108,7 @@ export class IssueReporter extends Disposable { } } - this.issueMainService.$getSystemInfo().then(info => { + this.processMainService.$getSystemInfo().then(info => { this.issueReporterModel.update({ systemInfo: info }); this.receivedSystemInfo = true; @@ -115,7 +116,7 @@ export class IssueReporter extends Disposable { this.updatePreviewButtonState(); }); if (configuration.data.issueType === IssueType.PerformanceIssue) { - this.issueMainService.$getPerformanceInfo().then(info => { + this.processMainService.$getPerformanceInfo().then(info => { this.updatePerformanceInfo(info as Partial); }); } @@ -286,7 +287,7 @@ export class IssueReporter extends Disposable { const issueType = parseInt((event.target).value); this.issueReporterModel.update({ issueType: issueType }); if (issueType === IssueType.PerformanceIssue && !this.receivedPerformanceInfo) { - this.issueMainService.$getPerformanceInfo().then(info => { + this.processMainService.$getPerformanceInfo().then(info => { this.updatePerformanceInfo(info as Partial); }); } @@ -458,7 +459,7 @@ export class IssueReporter extends Disposable { // Manually perform the selection if (isMacintosh) { if (cmdOrCtrlKey && e.keyCode === 65 && e.target) { - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + if (isHTMLInputElement(e.target) || isHTMLTextAreaElement(e.target)) { (e.target).select(); } } @@ -1356,7 +1357,7 @@ export class IssueReporter extends Disposable { const showLoading = this.getElementById('ext-loading')!; show(showLoading); while (showLoading.firstChild) { - showLoading.removeChild(showLoading.firstChild); + showLoading.firstChild.remove(); } showLoading.append(element); @@ -1377,7 +1378,7 @@ export class IssueReporter extends Disposable { const hideLoading = this.getElementById('ext-loading')!; hide(hideLoading); if (hideLoading.firstChild) { - hideLoading.removeChild(element); + element.remove(); } this.renderBlocks(); } diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService2.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts similarity index 90% rename from src/vs/code/electron-sandbox/issue/issueReporterService2.ts rename to src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts index 51bdfe923b8..02b3adb817d 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService2.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { $, isHTMLInputElement, isHTMLTextAreaElement, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; import { Codicon } from 'vs/base/common/codicons'; import { groupBy } from 'vs/base/common/collections'; @@ -10,13 +10,13 @@ import { CancellationError } from 'vs/base/common/errors'; import { isMacintosh } from 'vs/base/common/platform'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { IssueReporterData as IssueReporterModelData } from 'vs/code/browser/issue/issueReporterModel'; -import { BaseIssueReporterService, hide, show } from 'vs/code/browser/issue/issue'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; -import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; +import { IIssueMainService, IProcessMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; import { INativeHostService } from 'vs/platform/native/common/native'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/window/electron-sandbox/window'; +import { BaseIssueReporterService, hide, show } from 'vs/workbench/contrib/issue/browser/issue'; +import { IssueReporterData as IssueReporterModelData } from 'vs/workbench/contrib/issue/browser/issueReporterModel'; // GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe. // ref https://github.com/microsoft/vscode/issues/159191 @@ -24,14 +24,17 @@ const MAX_URL_LENGTH = 7500; export class IssueReporter2 extends BaseIssueReporterService { + private readonly processMainService: IProcessMainService; constructor( private readonly configuration: IssueReporterWindowConfiguration, @INativeHostService private readonly nativeHostService: INativeHostService, - @IIssueMainService issueMainService: IIssueMainService + @IIssueMainService issueMainService: IIssueMainService, + @IProcessMainService processMainService: IProcessMainService ) { super(configuration.disableExtensions, configuration.data, configuration.os, configuration.product, mainWindow, false, issueMainService); - this.issueMainService.$getSystemInfo().then(info => { + this.processMainService = processMainService; + this.processMainService.$getSystemInfo().then(info => { this.issueReporterModel.update({ systemInfo: info }); this.receivedSystemInfo = true; @@ -39,7 +42,7 @@ export class IssueReporter2 extends BaseIssueReporterService { this.updatePreviewButtonState(); }); if (configuration.data.issueType === IssueType.PerformanceIssue) { - this.issueMainService.$getPerformanceInfo().then(info => { + this.processMainService.$getPerformanceInfo().then(info => { this.updatePerformanceInfo(info as Partial); }); } @@ -81,6 +84,26 @@ export class IssueReporter2 extends BaseIssueReporterService { public override setEventHandlers(): void { super.setEventHandlers(); + this.addEventListener('issue-type', 'change', (event: Event) => { + const issueType = parseInt((event.target).value); + this.issueReporterModel.update({ issueType: issueType }); + if (issueType === IssueType.PerformanceIssue && !this.receivedPerformanceInfo) { + this.processMainService.$getPerformanceInfo().then(info => { + this.updatePerformanceInfo(info as Partial); + }); + } + + // Resets placeholder + const descriptionTextArea = this.getElementById('issue-title'); + if (descriptionTextArea) { + descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); + } + + this.updatePreviewButtonState(); + this.setSourceOptions(); + this.render(); + }); + // Keep all event listerns involving window and issue creation this.previewButton.onDidClick(async () => { this.delayedSubmit.trigger(async () => { @@ -146,7 +169,7 @@ export class IssueReporter2 extends BaseIssueReporterService { // Manually perform the selection if (isMacintosh) { if (cmdOrCtrlKey && e.key === 'a' && e.target) { - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + if (isHTMLInputElement(e.target) || isHTMLTextAreaElement(e.target)) { (e.target).select(); } } @@ -215,6 +238,7 @@ export class IssueReporter2 extends BaseIssueReporterService { if (this.issueReporterModel.fileOnExtension()) { this.addEventListener('extension-selector', 'change', _ => { this.validateInput('extension-selector'); + this.validateInput('description'); }); } @@ -464,7 +488,7 @@ export class IssueReporter2 extends BaseIssueReporterService { const showLoading = this.getElementById('ext-loading')!; show(showLoading); while (showLoading.firstChild) { - showLoading.removeChild(showLoading.firstChild); + showLoading.firstChild.remove(); } showLoading.append(element); @@ -485,7 +509,7 @@ export class IssueReporter2 extends BaseIssueReporterService { const hideLoading = this.getElementById('ext-loading')!; hide(hideLoading); if (hideLoading.firstChild) { - hideLoading.removeChild(element); + element.remove(); } this.renderBlocks(); } diff --git a/src/vs/workbench/services/issue/electron-sandbox/issueService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts similarity index 74% rename from src/vs/workbench/services/issue/electron-sandbox/issueService.ts rename to src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts index 410ea55fdfd..651dfa6c509 100644 --- a/src/vs/workbench/services/issue/electron-sandbox/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts @@ -4,23 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { getZoomLevel } from 'vs/base/browser/browser'; -import { platform } from 'vs/base/common/process'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionIdentifier, ExtensionType, ExtensionIdentifierSet } from 'vs/platform/extensions/common/extensions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, ProcessExplorerData } from 'vs/platform/issue/common/issue'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { activeContrastBorder, buttonBackground, buttonForeground, buttonHoverBackground, editorBackground, editorForeground, foreground, inputActiveOptionBorder, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, listActiveSelectionBackground, listActiveSelectionForeground, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles } from 'vs/platform/issue/common/issue'; +import { buttonBackground, buttonForeground, buttonHoverBackground, foreground, inputActiveOptionBorder, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; -import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; -import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; import { mainWindow } from 'vs/base/browser/window'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -34,9 +31,7 @@ export class NativeIssueService implements IWorkbenchIssueService { @IThemeService private readonly themeService: IThemeService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, - @IProductService private readonly productService: IProductService, @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IIntegrityService private readonly integrityService: IIntegrityService, @@ -46,11 +41,10 @@ export class NativeIssueService implements IWorkbenchIssueService { ipcRenderer.on('vscode:triggerReporterMenu', async (event, arg) => { const extensionId = arg.extensionId; - // creates menu from contributed - const menu = this.menuService.createMenu(MenuId.IssueReporter, this.contextKeyService); + // gets menu from contributed + const actions = this.menuService.getMenuActions(MenuId.IssueReporter, this.contextKeyService, { renderShortTitle: true }).flatMap(entry => entry[1]); // render menu and dispose - const actions = menu.getActions({ renderShortTitle: true }).flatMap(entry => entry[1]); actions.forEach(async action => { try { if (action.item && 'source' in action.item && action.item.source?.id === extensionId) { @@ -66,7 +60,6 @@ export class NativeIssueService implements IWorkbenchIssueService { // send undefined to indicate no action was taken ipcRenderer.send(`vscode:triggerReporterMenuResponse:${extensionId}`, undefined); } - menu.dispose(); }); } @@ -153,34 +146,6 @@ export class NativeIssueService implements IWorkbenchIssueService { return this.issueMainService.openReporter(issueReporterData); } - openProcessExplorer(): Promise { - const theme = this.themeService.getColorTheme(); - const data: ProcessExplorerData = { - pid: this.environmentService.mainPid, - zoomLevel: getZoomLevel(mainWindow), - styles: { - backgroundColor: getColor(theme, editorBackground), - color: getColor(theme, editorForeground), - listHoverBackground: getColor(theme, listHoverBackground), - listHoverForeground: getColor(theme, listHoverForeground), - listFocusBackground: getColor(theme, listFocusBackground), - listFocusForeground: getColor(theme, listFocusForeground), - listFocusOutline: getColor(theme, listFocusOutline), - listActiveSelectionBackground: getColor(theme, listActiveSelectionBackground), - listActiveSelectionForeground: getColor(theme, listActiveSelectionForeground), - listHoverOutline: getColor(theme, activeContrastBorder), - scrollbarShadowColor: getColor(theme, scrollbarShadow), - scrollbarSliderActiveBackgroundColor: getColor(theme, scrollbarSliderActiveBackground), - scrollbarSliderBackgroundColor: getColor(theme, scrollbarSliderBackground), - scrollbarSliderHoverBackgroundColor: getColor(theme, scrollbarSliderHoverBackground), - }, - platform: platform, - applicationName: this.productService.applicationName - }; - return this.issueMainService.openProcessExplorer(data); - } - - } export function getIssueReporterStyles(theme: IColorTheme): IssueReporterStyles { diff --git a/src/vs/code/electron-sandbox/issue/media/issueReporter.css b/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css similarity index 100% rename from src/vs/code/electron-sandbox/issue/media/issueReporter.css rename to src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/process.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/process.contribution.ts new file mode 100644 index 00000000000..43fe8fb39d2 --- /dev/null +++ b/src/vs/workbench/contrib/issue/electron-sandbox/process.contribution.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize, localize2 } from 'vs/nls'; +import { MenuRegistry, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { IWorkbenchProcessService } from 'vs/workbench/contrib/issue/common/issue'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { INativeHostService } from 'vs/platform/native/common/native'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProcessMainService } from 'vs/platform/issue/common/issue'; +import 'vs/workbench/contrib/issue/electron-sandbox/processService'; +import 'vs/workbench/contrib/issue/electron-sandbox/issueMainService'; + + +//#region Commands + +class OpenProcessExplorer extends Action2 { + + static readonly ID = 'workbench.action.openProcessExplorer'; + + constructor() { + super({ + id: OpenProcessExplorer.ID, + title: localize2('openProcessExplorer', 'Open Process Explorer'), + category: Categories.Developer, + f1: true + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const processService = accessor.get(IWorkbenchProcessService); + + return processService.openProcessExplorer(); + } +} +registerAction2(OpenProcessExplorer); +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '5_tools', + command: { + id: OpenProcessExplorer.ID, + title: localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer") + }, + order: 2 +}); + +class StopTracing extends Action2 { + + static readonly ID = 'workbench.action.stopTracing'; + + constructor() { + super({ + id: StopTracing.ID, + title: localize2('stopTracing', 'Stop Tracing'), + category: Categories.Developer, + f1: true + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const processService = accessor.get(IProcessMainService); + const environmentService = accessor.get(INativeEnvironmentService); + const dialogService = accessor.get(IDialogService); + const nativeHostService = accessor.get(INativeHostService); + const progressService = accessor.get(IProgressService); + + if (!environmentService.args.trace) { + const { confirmed } = await dialogService.confirm({ + message: localize('stopTracing.message', "Tracing requires to launch with a '--trace' argument"), + primaryButton: localize({ key: 'stopTracing.button', comment: ['&& denotes a mnemonic'] }, "&&Relaunch and Enable Tracing"), + }); + + if (confirmed) { + return nativeHostService.relaunch({ addArgs: ['--trace'] }); + } + } + + await progressService.withProgress({ + location: ProgressLocation.Dialog, + title: localize('stopTracing.title', "Creating trace file..."), + cancellable: false, + detail: localize('stopTracing.detail', "This can take up to one minute to complete.") + }, () => processService.stopTracing()); + } +} +registerAction2(StopTracing); + +CommandsRegistry.registerCommand('_issues.getSystemStatus', (accessor) => { + return accessor.get(IProcessMainService).getSystemStatus(); +}); +//#endregion diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/processService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/processService.ts new file mode 100644 index 00000000000..60ebd4f898f --- /dev/null +++ b/src/vs/workbench/contrib/issue/electron-sandbox/processService.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getZoomLevel } from 'vs/base/browser/browser'; +import { platform } from 'vs/base/common/process'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IProcessMainService, ProcessExplorerData } from 'vs/platform/issue/common/issue'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { activeContrastBorder, editorBackground, editorForeground, listActiveSelectionBackground, listActiveSelectionForeground, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { IWorkbenchProcessService } from 'vs/workbench/contrib/issue/common/issue'; +import { mainWindow } from 'vs/base/browser/window'; + +export class ProcessService implements IWorkbenchProcessService { + declare readonly _serviceBrand: undefined; + + constructor( + @IProcessMainService private readonly processMainService: IProcessMainService, + @IThemeService private readonly themeService: IThemeService, + @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, + @IProductService private readonly productService: IProductService, + ) { } + + openProcessExplorer(): Promise { + const theme = this.themeService.getColorTheme(); + const data: ProcessExplorerData = { + pid: this.environmentService.mainPid, + zoomLevel: getZoomLevel(mainWindow), + styles: { + backgroundColor: getColor(theme, editorBackground), + color: getColor(theme, editorForeground), + listHoverBackground: getColor(theme, listHoverBackground), + listHoverForeground: getColor(theme, listHoverForeground), + listFocusBackground: getColor(theme, listFocusBackground), + listFocusForeground: getColor(theme, listFocusForeground), + listFocusOutline: getColor(theme, listFocusOutline), + listActiveSelectionBackground: getColor(theme, listActiveSelectionBackground), + listActiveSelectionForeground: getColor(theme, listActiveSelectionForeground), + listHoverOutline: getColor(theme, activeContrastBorder), + scrollbarShadowColor: getColor(theme, scrollbarShadow), + scrollbarSliderActiveBackgroundColor: getColor(theme, scrollbarSliderActiveBackground), + scrollbarSliderBackgroundColor: getColor(theme, scrollbarSliderBackground), + scrollbarSliderHoverBackgroundColor: getColor(theme, scrollbarSliderHoverBackground), + }, + platform: platform, + applicationName: this.productService.applicationName + }; + return this.processMainService.openProcessExplorer(data); + } + + +} + +function getColor(theme: IColorTheme, key: string): string | undefined { + const color = theme.getColor(key); + return color ? color.toString() : undefined; +} + +registerSingleton(IWorkbenchProcessService, ProcessService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index fd3f51d3560..daf83ca4cb0 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -122,7 +122,7 @@ class LanguageStatus { this._update(); this._storeState(); } - }, this._disposables); + }, undefined, this._disposables); } @@ -231,7 +231,7 @@ class LanguageStatus { const targetWindow = dom.getWindow(editor?.getContainerDomNode()); const node = targetWindow.document.querySelector('.monaco-workbench .statusbar DIV#status\\.languageStatus A>SPAN.codicon'); const container = targetWindow.document.querySelector('.monaco-workbench .statusbar DIV#status\\.languageStatus'); - if (node instanceof HTMLElement && container) { + if (dom.isHTMLElement(node) && container) { const _wiggle = 'wiggle'; const _flash = 'flash'; if (!isOneBusy) { @@ -251,7 +251,7 @@ class LanguageStatus { // use that as signal that the user has interacted/learned language status items work if (!userHasInteractedWithStatus) { const hoverTarget = targetWindow.document.querySelector('.monaco-workbench .context-view'); - if (hoverTarget instanceof HTMLElement) { + if (dom.isHTMLElement(hoverTarget)) { const observer = new MutationObserver(() => { if (targetWindow.document.contains(element)) { this._interactionCounter.increment(); diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 4354ad022df..25433bee4a4 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -113,6 +113,7 @@ .monaco-workbench .hover-language-status > .element .right .monaco-link { margin: auto 0; white-space: nowrap; + text-decoration: var(--text-link-decoration); } .monaco-workbench .hover-language-status > .element .right .monaco-action-bar:not(:first-child) { diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts index c768ab602f0..098357445d0 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts @@ -33,6 +33,7 @@ import { getLocalHistoryDateFormatter, LOCAL_HISTORY_ICON_RESTORE, LOCAL_HISTORY import { IPathService } from 'vs/workbench/services/path/common/pathService'; const LOCAL_HISTORY_CATEGORY = localize2('localHistory.category', 'Local History'); +const CTX_LOCAL_HISTORY_ENABLED = ContextKeyExpr.has('config.workbench.localHistory.enabled'); export interface ITimelineCommandArgument { uri: URI; @@ -316,7 +317,8 @@ registerAction2(class extends Action2 { id: 'workbench.action.localHistory.restoreViaPicker', title: localize2('localHistory.restoreViaPicker', 'Find Entry to Restore'), f1: true, - category: LOCAL_HISTORY_CATEGORY + category: LOCAL_HISTORY_CATEGORY, + precondition: CTX_LOCAL_HISTORY_ENABLED }); } async run(accessor: ServicesAccessor): Promise { @@ -402,7 +404,7 @@ registerAction2(class extends Action2 { } }); -MenuRegistry.appendMenuItem(MenuId.TimelineTitle, { command: { id: 'workbench.action.localHistory.restoreViaPicker', title: localize2('localHistory.restoreViaPickerMenu', 'Local History: Find Entry to Restore...') }, group: 'submenu', order: 1 }); +MenuRegistry.appendMenuItem(MenuId.TimelineTitle, { command: { id: 'workbench.action.localHistory.restoreViaPicker', title: localize2('localHistory.restoreViaPickerMenu', 'Local History: Find Entry to Restore...') }, group: 'submenu', order: 1, when: CTX_LOCAL_HISTORY_ENABLED }); //#endregion @@ -499,7 +501,8 @@ registerAction2(class extends Action2 { id: 'workbench.action.localHistory.deleteAll', title: localize2('localHistory.deleteAll', 'Delete All'), f1: true, - category: LOCAL_HISTORY_CATEGORY + category: LOCAL_HISTORY_CATEGORY, + precondition: CTX_LOCAL_HISTORY_ENABLED }); } async run(accessor: ServicesAccessor): Promise { @@ -534,7 +537,7 @@ registerAction2(class extends Action2 { title: localize2('localHistory.create', 'Create Entry'), f1: true, category: LOCAL_HISTORY_CATEGORY, - precondition: ActiveEditorContext + precondition: ContextKeyExpr.and(CTX_LOCAL_HISTORY_ENABLED, ActiveEditorContext) }); } async run(accessor: ServicesAccessor): Promise { diff --git a/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts b/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts index b603ed444e6..a93fc9f9901 100644 --- a/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts +++ b/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts @@ -103,7 +103,7 @@ class NativeLocalizationWorkbenchContribution extends BaseLocalizationWorkbenchC if (!this.galleryService.isEnabled()) { return; } - if (!language || !locale || locale === 'en' || locale.indexOf('en-') === 0) { + if (!language || !locale || platform.Language.isDefaultVariant()) { return; } if (locale.startsWith(language) || languagePackSuggestionIgnoreList.includes(locale)) { diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index b96ba30f68c..a5c42063463 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -70,11 +70,11 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { super(); const contextKey = CONTEXT_LOG_LEVEL.bindTo(contextKeyService); contextKey.set(LogLevelToString(loggerService.getLogLevel())); - loggerService.onDidChangeLogLevel(e => { + this._register(loggerService.onDidChangeLogLevel(e => { if (isLogLevel(e)) { contextKey.set(LogLevelToString(loggerService.getLogLevel())); } - }); + })); this.onDidAddLoggers(loggerService.getRegisteredLoggers()); this._register(loggerService.onDidChangeLoggers(({ added, removed }) => { diff --git a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index 9b18b5cd028..b1c6b962fab 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -13,7 +13,6 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { tokenizeToString } from 'vs/editor/common/languages/textToHtmlTokenizer'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { escape } from 'vs/base/common/strings'; -import { SimpleSettingRenderer } from 'vs/workbench/contrib/markdown/browser/markdownSettingRenderer'; export const DEFAULT_MARKDOWN_STYLES = ` body { @@ -33,7 +32,7 @@ img { } a { - text-decoration: none; + text-decoration: var(--text-link-decoration); } a:hover { @@ -184,6 +183,13 @@ function sanitize(documentContent: string, allowUnknownProtocols: boolean): stri } } +interface IRenderMarkdownDocumentOptions { + readonly shouldSanitize?: boolean; + readonly allowUnknownProtocols?: boolean; + readonly renderer?: marked.Renderer; + readonly token?: CancellationToken; +} + /** * Renders a string of markdown as a document. * @@ -193,10 +199,7 @@ export async function renderMarkdownDocument( text: string, extensionService: IExtensionService, languageService: ILanguageService, - shouldSanitize: boolean = true, - allowUnknownProtocols: boolean = false, - token?: CancellationToken, - settingRenderer?: SimpleSettingRenderer + options?: IRenderMarkdownDocumentOptions ): Promise { const highlight = (code: string, lang: string | undefined, callback: ((error: any, code: string) => void) | undefined): any => { @@ -210,7 +213,7 @@ export async function renderMarkdownDocument( } extensionService.whenInstalledExtensionsRegistered().then(async () => { - if (token?.isCancellationRequested) { + if (options?.token?.isCancellationRequested) { callback(null, ''); return; } @@ -222,16 +225,11 @@ export async function renderMarkdownDocument( return ''; }; - const renderer = new marked.Renderer(); - if (settingRenderer) { - renderer.html = settingRenderer.getHtmlRenderer(); - } - return new Promise((resolve, reject) => { - marked(text, { highlight, renderer }, (err, value) => err ? reject(err) : resolve(value)); + marked(text, { highlight, renderer: options?.renderer }, (err, value) => err ? reject(err) : resolve(value)); }).then(raw => { - if (shouldSanitize) { - return sanitize(raw, allowUnknownProtocols); + if (options?.shouldSanitize ?? true) { + return sanitize(raw, options?.allowUnknownProtocols ?? false); } else { return raw; } diff --git a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts index 095a2978564..fe1c3c04e26 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts @@ -4,22 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IPreferencesService, ISetting, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; +import { IPreferencesService, ISetting } from 'vs/workbench/services/preferences/common/preferences'; import { settingKeyToDisplayFormat } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { DefaultSettings } from 'vs/workbench/services/preferences/common/preferencesModels'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IAction } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; - -const codeSettingRegex = /^/; +import { Schemas } from 'vs/base/common/network'; export class SimpleSettingRenderer { - private _defaultSettings: DefaultSettings; + private readonly codeSettingRegex: RegExp; + private _updatedSettings = new Map(); // setting ID to user's original setting value private _encounteredSettings = new Map(); // setting ID to setting private _featuredSettings = new Map(); // setting ID to feature value @@ -29,9 +27,9 @@ export class SimpleSettingRenderer { @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IPreferencesService private readonly _preferencesService: IPreferencesService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IClipboardService private readonly _clipboardService: IClipboardService + @IClipboardService private readonly _clipboardService: IClipboardService, ) { - this._defaultSettings = new DefaultSettings([], ConfigurationTarget.USER); + this.codeSettingRegex = new RegExp(`^`); } get featuredSettingStates(): Map { @@ -44,12 +42,12 @@ export class SimpleSettingRenderer { getHtmlRenderer(): (html: string) => string { return (html): string => { - const match = codeSettingRegex.exec(html); + const match = this.codeSettingRegex.exec(html); if (match && match.length === 4) { const settingId = match[2]; const rendered = this.render(settingId, match[3]); if (rendered) { - html = html.replace(codeSettingRegex, rendered); + html = html.replace(this.codeSettingRegex, rendered); } } return html; @@ -60,25 +58,11 @@ export class SimpleSettingRenderer { return `${Schemas.codeSetting}://${settingId}${value ? `/${value}` : ''}`; } - private settingsGroups: ISettingsGroup[] | undefined = undefined; private getSetting(settingId: string): ISetting | undefined { - if (!this.settingsGroups) { - this.settingsGroups = this._defaultSettings.getSettingsGroups(); - } if (this._encounteredSettings.has(settingId)) { return this._encounteredSettings.get(settingId); } - for (const group of this.settingsGroups) { - for (const section of group.sections) { - for (const setting of section.settings) { - if (setting.key === settingId) { - this._encounteredSettings.set(settingId, setting); - return setting; - } - } - } - } - return undefined; + return this._preferencesService.getSetting(settingId); } parseValue(settingId: string, value: string): any { diff --git a/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts b/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts index 55e38f3d018..1889507c974 100644 --- a/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts +++ b/src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IAction } from 'vs/base/common/actions'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -58,7 +58,15 @@ suite('Markdown Setting Renderer Test', () => { suiteSetup(() => { configurationService = new MarkdownConfigurationService(); - preferencesService = {}; + preferencesService = { + getSetting: (setting) => { + let type = 'boolean'; + if (setting.includes('string')) { + type = 'string'; + } + return { type, key: setting }; + } + }; contextMenuService = {}; Registry.as(Extensions.Configuration).registerConfiguration(configuration); settingRenderer = new SimpleSettingRenderer(configurationService, contextMenuService, preferencesService, { publicLog2: () => { } } as any, { writeText: async () => { } } as any); @@ -70,10 +78,10 @@ suite('Markdown Setting Renderer Test', () => { test('render code setting button with value', () => { const htmlRenderer = settingRenderer.getHtmlRenderer(); - const htmlNoValue = ''; + const htmlNoValue = ''; const renderedHtmlNoValue = htmlRenderer(htmlNoValue); assert.strictEqual(renderedHtmlNoValue, - ` + ` example.booleanSetting `); diff --git a/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts b/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts index e7355a0dd3d..c97455cb4ed 100644 --- a/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts +++ b/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts @@ -111,7 +111,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'type': 'object', 'properties': { 'problems.decorations.enabled': { - 'markdownDescription': localize('markers.showOnFile', "Show Errors & Warnings on files and folder. Overwritten by `#problems.visibility#` when it is off."), + 'markdownDescription': localize('markers.showOnFile', "Show Errors & Warnings on files and folder. Overwritten by {0} when it is off.", '`#problems.visibility#`'), 'type': 'boolean', 'default': true } diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 40c99ee3457..1a3dec090cc 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -50,7 +50,7 @@ import { unsupportedSchemas } from 'vs/platform/markers/common/markerService'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import Severity from 'vs/base/common/severity'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; interface IResourceMarkersTemplateData { @@ -281,7 +281,7 @@ class MarkerWidget extends Disposable { private readonly icon: HTMLElement; private readonly iconContainer: HTMLElement; private readonly messageAndDetailsContainer: HTMLElement; - private readonly messageAndDetailsContainerHover: IUpdatableHover; + private readonly messageAndDetailsContainerHover: IManagedHover; private readonly disposables = this._register(new DisposableStore()); constructor( @@ -302,7 +302,7 @@ class MarkerWidget extends Disposable { this.iconContainer = dom.append(parent, dom.$('')); this.icon = dom.append(this.iconContainer, dom.$('')); this.messageAndDetailsContainer = dom.append(parent, dom.$('.marker-message-details-container')); - this.messageAndDetailsContainerHover = this._register(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.messageAndDetailsContainer, '')); + this.messageAndDetailsContainerHover = this._register(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.messageAndDetailsContainer, '')); } render(element: Marker, filterData: MarkerFilterData | undefined): void { diff --git a/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts b/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts index b8334c948d6..bec693957c3 100644 --- a/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts +++ b/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers'; import { MarkersModel, Marker, ResourceMarkers, RelatedInformation } from 'vs/workbench/contrib/markers/browser/markersModel'; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts index b043f0840a1..279baa80c14 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts @@ -126,7 +126,7 @@ export class TempFileMergeEditorModeFactory implements IMergeEditorInputModelFac class TempFileMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel { private readonly savedAltVersionId = observableValue(this, this.model.resultTextModel.getAlternativeVersionId()); - private readonly altVersionId = observableFromEvent( + private readonly altVersionId = observableFromEvent(this, e => this.model.resultTextModel.onDidChangeContent(e), () => /** @description getAlternativeVersionId */ this.model.resultTextModel.getAlternativeVersionId() @@ -340,7 +340,7 @@ export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFa } class WorkspaceMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel { - public readonly isDirty = observableFromEvent( + public readonly isDirty = observableFromEvent(this, Event.any(this.resultTextFileModel.onDidChangeDirty, this.resultTextFileModel.onDidSaveError), () => /** @description isDirty */ this.resultTextFileModel.isDirty() ); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/colors.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/colors.ts index cc6e5700812..f8d1d8b6fcd 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/colors.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/colors.ts @@ -8,7 +8,7 @@ import { mergeCurrentHeaderBackground, mergeIncomingHeaderBackground, registerCo export const diff = registerColor( 'mergeEditor.change.background', - { dark: '#9bb95533', light: '#9bb95533', hcDark: '#9bb95533', hcLight: '#9bb95533', }, + '#9bb95533', localize('mergeEditor.change.background', 'The background color for changes.') ); @@ -38,49 +38,49 @@ export const conflictBorderUnhandledUnfocused = registerColor( export const conflictBorderUnhandledFocused = registerColor( 'mergeEditor.conflict.unhandledFocused.border', - { dark: '#ffa600', light: '#ffa600', hcDark: '#ffa600', hcLight: '#ffa600', }, + '#ffa600', localize('mergeEditor.conflict.unhandledFocused.border', 'The border color of unhandled focused conflicts.') ); export const conflictBorderHandledUnfocused = registerColor( 'mergeEditor.conflict.handledUnfocused.border', - { dark: '#86868649', light: '#86868649', hcDark: '#86868649', hcLight: '#86868649', }, + '#86868649', localize('mergeEditor.conflict.handledUnfocused.border', 'The border color of handled unfocused conflicts.') ); export const conflictBorderHandledFocused = registerColor( 'mergeEditor.conflict.handledFocused.border', - { dark: '#c1c1c1cc', light: '#c1c1c1cc', hcDark: '#c1c1c1cc', hcLight: '#c1c1c1cc', }, + '#c1c1c1cc', localize('mergeEditor.conflict.handledFocused.border', 'The border color of handled focused conflicts.') ); export const handledConflictMinimapOverViewRulerColor = registerColor( 'mergeEditor.conflict.handled.minimapOverViewRuler', - { dark: '#adaca8ee', light: '#adaca8ee', hcDark: '#adaca8ee', hcLight: '#adaca8ee', }, + '#adaca8ee', localize('mergeEditor.conflict.handled.minimapOverViewRuler', 'The foreground color for changes in input 1.') ); export const unhandledConflictMinimapOverViewRulerColor = registerColor( 'mergeEditor.conflict.unhandled.minimapOverViewRuler', - { dark: '#fcba03FF', light: '#fcba03FF', hcDark: '#fcba03FF', hcLight: '#fcba03FF', }, + '#fcba03FF', localize('mergeEditor.conflict.unhandled.minimapOverViewRuler', 'The foreground color for changes in input 1.') ); export const conflictingLinesBackground = registerColor( 'mergeEditor.conflictingLines.background', - { dark: '#ffea0047', light: '#ffea0047', hcDark: '#ffea0047', hcLight: '#ffea0047', }, + '#ffea0047', localize('mergeEditor.conflictingLines.background', 'The background of the "Conflicting Lines" text.') ); const contentTransparency = 0.4; export const conflictInput1Background = registerColor( 'mergeEditor.conflict.input1.background', - { dark: transparent(mergeCurrentHeaderBackground, contentTransparency), light: transparent(mergeCurrentHeaderBackground, contentTransparency), hcDark: transparent(mergeCurrentHeaderBackground, contentTransparency), hcLight: transparent(mergeCurrentHeaderBackground, contentTransparency) }, + transparent(mergeCurrentHeaderBackground, contentTransparency), localize('mergeEditor.conflict.input1.background', 'The background color of decorations in input 1.') ); export const conflictInput2Background = registerColor( 'mergeEditor.conflict.input2.background', - { dark: transparent(mergeIncomingHeaderBackground, contentTransparency), light: transparent(mergeIncomingHeaderBackground, contentTransparency), hcDark: transparent(mergeIncomingHeaderBackground, contentTransparency), hcLight: transparent(mergeIncomingHeaderBackground, contentTransparency) }, + transparent(mergeIncomingHeaderBackground, contentTransparency), localize('mergeEditor.conflict.input2.background', 'The background color of decorations in input 2.') ); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts index b751b6bc0ad..7d564cb4646 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts @@ -10,12 +10,12 @@ import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditor import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; export class EditorGutter extends Disposable { - private readonly scrollTop = observableFromEvent( + private readonly scrollTop = observableFromEvent(this, this._editor.onDidScrollChange, (e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop() ); private readonly isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0); - private readonly modelAttached = observableFromEvent( + private readonly modelAttached = observableFromEvent(this, this._editor.onDidChangeModel, (e) => /** @description editor.onDidChangeModel */ this._editor.hasModel() ); @@ -126,7 +126,7 @@ export class EditorGutter extends D for (const id of unusedIds) { const view = this.views.get(id)!; view.gutterItemView.dispose(); - this._domNode.removeChild(view.domNode); + view.domNode.remove(); this.views.delete(id); } } @@ -154,4 +154,3 @@ export interface IGutterItemView extends IDisposable update(item: T): void; layout(top: number, height: number, viewTop: number, viewHeight: number): void; } - diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index 29af08fbafe..b75ca359a2d 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -79,17 +79,17 @@ export abstract class CodeEditorView extends Disposable { this.editor.updateOptions(newOptions); } - public readonly isFocused = observableFromEvent( + public readonly isFocused = observableFromEvent(this, Event.any(this.editor.onDidBlurEditorWidget, this.editor.onDidFocusEditorWidget), () => /** @description editor.hasWidgetFocus */ this.editor.hasWidgetFocus() ); - public readonly cursorPosition = observableFromEvent( + public readonly cursorPosition = observableFromEvent(this, this.editor.onDidChangeCursorPosition, () => /** @description editor.getPosition */ this.editor.getPosition() ); - public readonly selection = observableFromEvent( + public readonly selection = observableFromEvent(this, this.editor.onDidChangeCursorSelection, () => /** @description editor.getSelections */ this.editor.getSelections() ); diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts index 85c475e3c2f..135d0d9a7ee 100644 --- a/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts +++ b/src/vs/workbench/contrib/mergeEditor/test/browser/mapping.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts index 0441e4483d3..a07bc71e675 100644 --- a/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts +++ b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IReader, transaction } from 'vs/base/common/observable'; import { isDefined } from 'vs/base/common/types'; diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/actions.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/actions.ts index 5298c06ac45..30e6c69a641 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/actions.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/actions.ts @@ -10,8 +10,9 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize2 } from 'vs/nls'; import { Action2, MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ITextEditorOptions, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; +import { resolveCommandsContext } from 'vs/workbench/browser/parts/editor/editorCommandsContext'; import { MultiDiffEditor } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor'; import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -37,21 +38,28 @@ export class GoToFileAction extends Action2 { const editorService = accessor.get(IEditorService); const activeEditorPane = editorService.activeEditorPane; let selections: Selection[] | undefined = undefined; - if (activeEditorPane instanceof MultiDiffEditor) { - const editor = activeEditorPane.tryGetCodeEditor(uri); - if (editor) { - selections = editor.editor.getSelections() ?? undefined; - } + if (!(activeEditorPane instanceof MultiDiffEditor)) { + return; } - const editor = await editorService.openEditor({ resource: uri }); - if (selections && (editor instanceof TextFileEditor)) { - const c = editor.getControl(); - if (c) { - c.setSelections(selections); - c.revealLineInCenter(selections[0].selectionStartLineNumber); - } + const editor = activeEditorPane.tryGetCodeEditor(uri); + if (editor) { + selections = editor.editor.getSelections() ?? undefined; } + + let targetUri = uri; + const item = activeEditorPane.findDocumentDiffItem(uri); + if (item && item.goToFileUri) { + targetUri = item.goToFileUri; + } + + await editorService.openEditor({ + resource: targetUri, + options: { + selection: selections?.[0], + selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport, + } satisfies ITextEditorOptions, + }); } } @@ -72,12 +80,17 @@ export class CollapseAllAction extends Action2 { }); } - async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const editorService = accessor.get(IEditorService); - const activeEditor = editorService.activeEditor; + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const resolvedContext = resolveCommandsContext(accessor, args); - if (activeEditor instanceof MultiDiffEditorInput) { - const viewModel = await activeEditor.getViewModel(); + const groupContext = resolvedContext.groupedEditors[0]; + if (!groupContext) { + return; + } + + const editor = groupContext.editors[0]; + if (editor instanceof MultiDiffEditorInput) { + const viewModel = await editor.getViewModel(); viewModel.collapseAll(); } } @@ -100,12 +113,17 @@ export class ExpandAllAction extends Action2 { }); } - async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const editorService = accessor.get(IEditorService); - const activeEditor = editorService.activeEditor; + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const resolvedContext = resolveCommandsContext(accessor, args); - if (activeEditor instanceof MultiDiffEditorInput) { - const viewModel = await activeEditor.getViewModel(); + const groupContext = resolvedContext.groupedEditors[0]; + if (!groupContext) { + return; + } + + const editor = groupContext.editors[0]; + if (editor instanceof MultiDiffEditorInput) { + const viewModel = await editor.getViewModel(); viewModel.expandAll(); } } diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts index ffe7a8738f5..2b5168dea9b 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts @@ -18,7 +18,7 @@ import { AbstractEditorWithViewState } from 'vs/workbench/browser/parts/editor/e import { ICompositeControl } from 'vs/workbench/common/composite'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; +import { IDocumentDiffItemWithMultiDiffEditorItem, MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { URI } from 'vs/base/common/uri'; @@ -27,6 +27,7 @@ import { IMultiDiffEditorOptions, IMultiDiffEditorViewState } from 'vs/editor/br import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditor } from 'vs/editor/common/editorCommon'; import { Range } from 'vs/editor/common/core/range'; +import { MultiDiffEditorItem } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffSourceResolverService'; export class MultiDiffEditor extends AbstractEditorWithViewState { static readonly ID = 'multiDiffEditor'; @@ -139,6 +140,13 @@ export class MultiDiffEditor extends AbstractEditorWithViewState new MultiDiffEditorItem( resource.originalUri ? URI.parse(resource.originalUri) : undefined, resource.modifiedUri ? URI.parse(resource.modifiedUri) : undefined, + resource.goToFileUri ? URI.parse(resource.goToFileUri) : undefined, )), false ); @@ -112,8 +114,9 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor label: this.label, multiDiffSourceUri: this.multiDiffSource.toString(), resources: this.initialResources?.map(resource => ({ - originalUri: resource.original?.toString(), - modifiedUri: resource.modified?.toString(), + originalUri: resource.originalUri?.toString(), + modifiedUri: resource.modifiedUri?.toString(), + goToFileUri: resource.goToFileUri?.toString(), })), }; } @@ -159,8 +162,8 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor try { [original, modified] = await Promise.all([ - r.original ? this._textModelService.createModelReference(r.original) : undefined, - r.modified ? this._textModelService.createModelReference(r.modified) : undefined, + r.originalUri ? this._textModelService.createModelReference(r.originalUri) : undefined, + r.modifiedUri ? this._textModelService.createModelReference(r.modifiedUri) : undefined, ]); if (original) { store2.add(original); } if (modified) { store2.add(modified); } @@ -171,8 +174,9 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor return undefined; } - const uri = (r.modified ?? r.original)!; - return new ConstLazyPromise({ + const uri = (r.modifiedUri ?? r.originalUri)!; + return new ConstLazyPromise({ + multiDiffEditorItem: r, original: original?.object.textEditorModel, modified: modified?.object.textEditorModel, get options() { @@ -187,7 +191,7 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor } }), }); - }, i => JSON.stringify([i.modified?.toString(), i.original?.toString()])); + }, i => JSON.stringify([i.modifiedUri?.toString(), i.originalUri?.toString()])); const documents = observableValue[]>('documents', []); @@ -239,8 +243,8 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor public readonly resources = derived(this, reader => this._resolvedSource.cachedPromiseResult.read(reader)?.data?.resources.read(reader)); private readonly _isDirtyObservables = mapObservableArrayCached(this, this.resources.map(r => r ?? []), res => { - const isModifiedDirty = res.modified ? isUriDirty(this._textFileService, res.modified) : constObservable(false); - const isOriginalDirty = res.original ? isUriDirty(this._textFileService, res.original) : constObservable(false); + const isModifiedDirty = res.modifiedUri ? isUriDirty(this._textFileService, res.modifiedUri) : constObservable(false); + const isOriginalDirty = res.originalUri ? isUriDirty(this._textFileService, res.originalUri) : constObservable(false); return derived(reader => /** @description modifiedDirty||originalDirty */ isModifiedDirty.read(reader) || isOriginalDirty.read(reader)); }, i => i.getKey()); private readonly _isDirtyObservable = derived(this, reader => this._isDirtyObservables.read(reader).some(isDirty => isDirty.read(reader))) @@ -291,6 +295,10 @@ export class MultiDiffEditorInput extends EditorInput implements ILanguageSuppor }; } +export interface IDocumentDiffItemWithMultiDiffEditorItem extends IDocumentDiffItem { + multiDiffEditorItem: MultiDiffEditorItem; +} + function isUriDirty(textFileService: ITextFileService, uri: URI) { return observableFromEvent( Event.filter(textFileService.files.onDidChangeDirty, e => e.resource.toString() === uri.toString()), @@ -361,6 +369,7 @@ interface ISerializedMultiDiffEditorInput { resources: { originalUri: string | undefined; modifiedUri: string | undefined; + goToFileUri: string | undefined; }[] | undefined; } diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffSourceResolverService.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffSourceResolverService.ts index 43f2f3eb899..44c3da5801d 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffSourceResolverService.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffSourceResolverService.ts @@ -33,16 +33,17 @@ export interface IResolvedMultiDiffSource { export class MultiDiffEditorItem { constructor( - readonly original: URI | undefined, - readonly modified: URI | undefined, + readonly originalUri: URI | undefined, + readonly modifiedUri: URI | undefined, + readonly goToFileUri: URI | undefined, ) { - if (!original && !modified) { + if (!originalUri && !modifiedUri) { throw new BugIndicatingError('Invalid arguments'); } } getKey(): string { - return JSON.stringify([this.modified?.toString(), this.original?.toString()]); + return JSON.stringify([this.modifiedUri?.toString(), this.originalUri?.toString()]); } } diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts index a53d4d6c346..43ff34c3ef5 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts @@ -61,11 +61,11 @@ export class ScmMultiDiffSourceResolver implements IMultiDiffSourceResolver { async resolveDiffSource(uri: URI): Promise { const { repositoryUri, groupId } = ScmMultiDiffSourceResolver.parseUri(uri)!; - const repository = await waitForState(observableFromEvent( + const repository = await waitForState(observableFromEvent(this, this._scmService.onDidAddRepository, () => [...this._scmService.repositories].find(r => r.provider.rootUri?.toString() === repositoryUri.toString())) ); - const group = await waitForState(observableFromEvent( + const group = await waitForState(observableFromEvent(this, repository.provider.onDidChangeResourceGroups, () => repository.provider.groups.find(g => g.id === groupId) )); @@ -76,7 +76,7 @@ export class ScmMultiDiffSourceResolver implements IMultiDiffSourceResolver { class ScmResolvedMultiDiffSource implements IResolvedMultiDiffSource { private readonly _resources = observableFromEvent( this._group.onDidChangeResources, - () => /** @description resources */ this._group.resources.map(e => new MultiDiffEditorItem(e.multiDiffEditorOriginalUri, e.multiDiffEditorModifiedUri)) + () => /** @description resources */ this._group.resources.map(e => new MultiDiffEditorItem(e.multiDiffEditorOriginalUri, e.multiDiffEditorModifiedUri, e.sourceUri)) ); readonly resources = new ValueWithChangeEventFromObservable(this._resources); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts index ab728db53f2..721b711f5d2 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts @@ -161,13 +161,13 @@ class ExecutionStateCellStatusBarItem extends Disposable { const state = runState?.state; const { lastRunSuccess } = internalMetadata; if (!state && lastRunSuccess) { - return [{ + return [{ text: `$(${successStateIcon.id})`, color: themeColorFromId(cellStatusIconSuccess), tooltip: localize('notebook.cell.status.success', "Success"), alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER - }]; + } satisfies INotebookCellStatusBarItem]; } else if (!state && lastRunSuccess === false) { return [{ text: `$(${errorStateIcon.id})`, @@ -177,22 +177,22 @@ class ExecutionStateCellStatusBarItem extends Disposable { priority: Number.MAX_SAFE_INTEGER }]; } else if (state === NotebookCellExecutionState.Pending || state === NotebookCellExecutionState.Unconfirmed) { - return [{ + return [{ text: `$(${pendingStateIcon.id})`, tooltip: localize('notebook.cell.status.pending', "Pending"), alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER - }]; + } satisfies INotebookCellStatusBarItem]; } else if (state === NotebookCellExecutionState.Executing) { const icon = runState?.didPause ? executingStateIcon : ThemeIcon.modify(executingStateIcon, 'spin'); - return [{ + return [{ text: `$(${icon.id})`, tooltip: localize('notebook.cell.status.executing', "Executing"), alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER - }]; + } satisfies INotebookCellStatusBarItem]; } return []; @@ -318,12 +318,12 @@ class TimerCellStatusBarItem extends Disposable { } - return { + return { text: formatCellDuration(duration, false), alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER - 5, tooltip - }; + } satisfies INotebookCellStatusBarItem; } override dispose() { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts index 2c4a1d43436..8aa8b3f3319 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts @@ -28,7 +28,7 @@ import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { ILogService } from 'vs/platform/log/common/log'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { showWindowLogActionId } from 'vs/workbench/services/log/common/logConstants'; -import { getActiveElement, getWindow, isAncestor } from 'vs/base/browser/dom'; +import { getActiveElement, getWindow, isAncestor, isHTMLElement } from 'vs/base/browser/dom'; let _logging: boolean = false; function toggleLogging() { @@ -360,7 +360,7 @@ export class NotebookClipboardContribution extends Disposable { const loggerService = accessor.get(ILogService); const activeElement = getActiveElement(); - if (activeElement instanceof HTMLElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { + if (isHTMLElement(activeElement) && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { _log(loggerService, '[NotebookEditor] focus is on input or textarea element, bypass'); return false; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts index eeef10e64d2..205bce806e1 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts @@ -8,6 +8,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -32,7 +33,8 @@ export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribu @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, @IChatAgentService chatAgentService: IChatAgentService, @ITelemetryService telemetryService: ITelemetryService, - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IContextMenuService contextMenuService: IContextMenuService ) { super( editor, @@ -44,7 +46,8 @@ export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribu inlineChatSessionService, chatAgentService, telemetryService, - productService + productService, + contextMenuService ); const activeEditor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts index 5ad7c674268..197230a61cc 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -10,8 +10,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { CENTER_ACTIVE_CELL } from 'vs/workbench/contrib/notebook/browser/contrib/navigation/arrow'; import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { SELECT_NOTEBOOK_INDENTATION_ID } from 'vs/workbench/contrib/notebook/browser/controller/editActions'; @@ -20,8 +19,9 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; class ImplictKernelSelector implements IDisposable { @@ -179,8 +179,6 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { } } -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(KernelStatus, LifecyclePhase.Restored); - export class ActiveCellStatus extends Disposable implements IWorkbenchContribution { private readonly _itemDisposables = this._register(new DisposableStore()); @@ -255,9 +253,7 @@ export class ActiveCellStatus extends Disposable implements IWorkbenchContributi } } -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ActiveCellStatus, LifecyclePhase.Restored); - -export class NotebookIndentationStatus extends Disposable implements IWorkbenchContribution { +export class NotebookIndentationStatus extends Disposable { private readonly _itemDisposables = this._register(new DisposableStore()); private readonly _accessor = this._register(new MutableDisposable()); @@ -339,4 +335,33 @@ export class NotebookIndentationStatus extends Disposable implements IWorkbenchC } } -registerWorkbenchContribution2(NotebookIndentationStatus.ID, NotebookIndentationStatus, WorkbenchPhase.AfterRestored); // TODO@Yoyokrazy -- unsure on the phase +export class NotebookEditorStatusContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'notebook.contrib.editorStatus'; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService + ) { + super(); + + // Main Editor Status + const mainInstantiationService = instantiationService.createChild(new ServiceCollection( + [IEditorService, editorService.createScoped('main', this._store)] + )); + this._register(mainInstantiationService.createInstance(KernelStatus)); + this._register(mainInstantiationService.createInstance(ActiveCellStatus)); + this._register(mainInstantiationService.createInstance(NotebookIndentationStatus)); + + // Auxiliary Editor Status + this._register(editorGroupService.onDidCreateAuxiliaryEditorPart(({ part, instantiationService, disposables }) => { + disposables.add(instantiationService.createInstance(KernelStatus)); + disposables.add(instantiationService.createInstance(ActiveCellStatus)); + disposables.add(instantiationService.createInstance(NotebookIndentationStatus)); + })); + } +} + + +registerWorkbenchContribution2(NotebookEditorStatusContribution.ID, NotebookEditorStatusContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts index a4bf2fd2d9f..cf98120913c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts @@ -3,19 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; +import { INotebookFindScope, NotebookFindScopeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -export interface INotebookFindFiltersChangeEvent { +export interface INotebookFindChangeEvent { markupInput?: boolean; markupPreview?: boolean; codeInput?: boolean; codeOutput?: boolean; + findScope?: boolean; } export class NotebookFindFilters extends Disposable { - private readonly _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; + private readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; private _markupInput: boolean = true; @@ -68,17 +70,31 @@ export class NotebookFindFilters extends Disposable { } } + private _findScope: INotebookFindScope = { findScopeType: NotebookFindScopeType.None }; + + get findScope(): INotebookFindScope { + return this._findScope; + } + + set findScope(value: INotebookFindScope) { + if (this._findScope !== value) { + this._findScope = value; + this._onDidChange.fire({ findScope: true }); + } + } + + private readonly _initialMarkupInput: boolean; private readonly _initialMarkupPreview: boolean; private readonly _initialCodeInput: boolean; private readonly _initialCodeOutput: boolean; - constructor( markupInput: boolean, markupPreview: boolean, codeInput: boolean, - codeOutput: boolean + codeOutput: boolean, + findScope: INotebookFindScope ) { super(); @@ -86,6 +102,7 @@ export class NotebookFindFilters extends Disposable { this._markupPreview = markupPreview; this._codeInput = codeInput; this._codeOutput = codeOutput; + this._findScope = findScope; this._initialMarkupInput = markupInput; this._initialMarkupPreview = markupPreview; @@ -94,6 +111,7 @@ export class NotebookFindFilters extends Disposable { } isModified(): boolean { + // do not include findInSelection or either selectedRanges in the check. This will incorrectly mark the filter icon as modified return ( this._markupInput !== this._initialMarkupInput || this._markupPreview !== this._initialMarkupPreview @@ -107,5 +125,6 @@ export class NotebookFindFilters extends Disposable { this._markupPreview = v.markupPreview; this._codeInput = v.codeInput; this._codeOutput = v.codeOutput; + this._findScope = v.findScope; } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts index 6fa5a4fea6c..214de0211a6 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts @@ -57,7 +57,6 @@ export class FindMatchDecorationModel extends Disposable { }); this._currentMatchCellDecorations = this._notebookEditor.deltaCellDecorations(this._currentMatchCellDecorations, [{ - ownerId: cell.handle, handle: cell.handle, options: { overviewRuler: { @@ -67,7 +66,7 @@ export class FindMatchDecorationModel extends Disposable { position: NotebookOverviewRulerLane.Center } } - } as INotebookDeltaDecoration]); + }]); return null; } @@ -80,7 +79,6 @@ export class FindMatchDecorationModel extends Disposable { this._currentMatchDecorations = { kind: 'output', index: index }; this._currentMatchCellDecorations = this._notebookEditor.deltaCellDecorations(this._currentMatchCellDecorations, [{ - ownerId: cell.handle, handle: cell.handle, options: { overviewRuler: { @@ -90,7 +88,7 @@ export class FindMatchDecorationModel extends Disposable { position: NotebookOverviewRulerLane.Center } } - } as INotebookDeltaDecoration]); + } satisfies INotebookDeltaDecoration]); return offset; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts index 6c0cca31b75..0ca2d422f53 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -3,21 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { findFirstIdxMonotonousOrArrLen } from 'vs/base/common/arraysFind'; import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; -import { INotebookEditor, CellEditState, CellFindMatchWithIndex, CellWebviewFindMatch, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; import { FindMatch } from 'vs/editor/common/model'; import { PrefixSumComputer } from 'vs/editor/common/model/prefixSumComputer'; import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/browser/findState'; -import { CellKind, INotebookSearchOptions, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { findFirstIdxMonotonousOrArrLen } from 'vs/base/common/arraysFind'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; import { FindMatchDecorationModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel'; +import { CellEditState, CellFindMatchWithIndex, CellWebviewFindMatch, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CellKind, INotebookFindOptions, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class CellFindMatchModel implements CellFindMatchWithIndex { readonly cell: ICellViewModel; @@ -115,11 +115,7 @@ export class FindModel extends Disposable { } private _updateCellStates(e: FindReplaceStateChangedEvent) { - if (!this._state.filters?.markupInput) { - return; - } - - if (!this._state.filters?.markupPreview) { + if (!this._state.filters?.markupInput || !this._state.filters?.markupPreview || !this._state.filters?.findScope) { return; } @@ -131,7 +127,7 @@ export class FindModel extends Disposable { } // search markup sources first to decide if a markup cell should be in editing mode const wordSeparators = this._configurationService.inspect('editor.wordSeparators').value; - const options: INotebookSearchOptions = { + const options: INotebookFindOptions = { regex: this._state.isRegex, wholeWord: this._state.wholeWord, caseSensitive: this._state.matchCase, @@ -139,7 +135,8 @@ export class FindModel extends Disposable { includeMarkupInput: true, includeCodeInput: false, includeMarkupPreview: false, - includeOutput: false + includeOutput: false, + findScope: this._state.filters?.findScope, }; const contentMatches = viewModel.find(this._state.searchString, options); @@ -478,7 +475,7 @@ export class FindModel extends Disposable { const val = this._state.searchString; const wordSeparators = this._configurationService.inspect('editor.wordSeparators').value; - const options: INotebookSearchOptions = { + const options: INotebookFindOptions = { regex: this._state.isRegex, wholeWord: this._state.wholeWord, caseSensitive: this._state.matchCase, @@ -486,7 +483,8 @@ export class FindModel extends Disposable { includeMarkupInput: this._state.filters?.markupInput ?? true, includeCodeInput: this._state.filters?.codeInput ?? true, includeMarkupPreview: !!this._state.filters?.markupPreview, - includeOutput: !!this._state.filters?.codeOutput + includeOutput: !!this._state.filters?.codeOutput, + findScope: this._state.filters?.findScope, }; ret = await this._notebookEditor.find(val, options, token); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/media/notebookFind.css b/src/vs/workbench/contrib/notebook/browser/contrib/find/media/notebookFind.css index fe115e23081..d61cc797497 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/media/notebookFind.css +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/media/notebookFind.css @@ -19,3 +19,7 @@ padding: 0 2px; box-sizing: border-box; } + +.monaco-workbench .nb-findScope { + background-color: var(--vscode-editor-findRangeHighlightBackground); +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts index f91d2ed8863..fb3d93d9d5e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts @@ -10,6 +10,7 @@ import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; import { FindStartFocusAction, getSelectionSearchString, IFindStartOptions, StartFindAction, StartFindReplaceAction } from 'vs/editor/contrib/find/browser/findController'; @@ -19,12 +20,12 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IShowNotebookFindWidgetOptions, NotebookFindContrib } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { INotebookCommandContext, NotebookMultiCellAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, NotebookFindScopeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INTERACTIVE_WINDOW_IS_ACTIVE_EDITOR, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; registerNotebookContribution(NotebookFindContrib.id, NotebookFindContrib); @@ -55,7 +56,7 @@ registerAction2(class extends Action2 { } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookMultiCellAction { constructor() { super({ id: 'notebook.find', @@ -68,7 +69,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor): Promise { + async runWithContext(accessor: ServicesAccessor, context: INotebookCommandContext): Promise { const editorService = accessor.get(IEditorService); const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); @@ -77,7 +78,7 @@ registerAction2(class extends Action2 { } const controller = editor.getContribution(NotebookFindContrib.id); - controller.show(); + controller.show(undefined, { findScope: { findScopeType: NotebookFindScopeType.None } }); } }); @@ -200,4 +201,3 @@ StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, codeE return false; }); - diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index d616161b309..8c4b59a688e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -3,51 +3,56 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; +import 'vs/css!./notebookFindReplaceWidget'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput'; import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; +import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Widget } from 'vs/base/browser/ui/widget'; +import { Action, ActionRunner, IAction, IActionRunner, Separator } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; import { KeyCode } from 'vs/base/common/keyCodes'; -import 'vs/css!./notebookFindReplaceWidget'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { isSafari } from 'vs/base/common/platform'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { Range } from 'vs/editor/common/core/range'; import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/browser/findState'; -import { findNextMatchIcon, findPreviousMatchIcon, findReplaceAllIcon, findReplaceIcon, SimpleButton } from 'vs/editor/contrib/find/browser/findWidget'; -import * as nls from 'vs/nls'; -import { ContextScopedReplaceInput, registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; +import { findNextMatchIcon, findPreviousMatchIcon, findReplaceAllIcon, findReplaceIcon, findSelectionIcon, SimpleButton } from 'vs/editor/contrib/find/browser/findWidget'; +import { parseReplaceString, ReplacePattern } from 'vs/editor/contrib/find/browser/replacePattern'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { ContextScopedReplaceInput, registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { defaultInputBoxStyles, defaultProgressBarStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { asCssVariable, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerIcon, widgetClose } from 'vs/platform/theme/common/iconRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { parseReplaceString, ReplacePattern } from 'vs/editor/contrib/find/browser/replacePattern'; -import { Codicon } from 'vs/base/common/codicons'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Action, ActionRunner, IAction, IActionRunner, Separator } from 'vs/base/common/actions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IMenu } from 'vs/platform/actions/common/actions'; -import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { filterIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; -import { isSafari } from 'vs/base/common/platform'; -import { ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; -import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { defaultInputBoxStyles, defaultProgressBarStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { IShowNotebookFindWidgetOptions } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, INotebookDeltaDecoration, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookFindScopeType, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; + const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous Match"); -// const NLS_FILTER_BTN_LABEL = nls.localize('label.findFilterButton', "Search in View"); const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next Match"); +const NLS_TOGGLE_SELECTION_FIND_TITLE = nls.localize('label.toggleSelectionFind', "Find in Selection"); const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace"); const NLS_REPLACE_INPUT_LABEL = nls.localize('label.replace', "Replace"); @@ -62,7 +67,7 @@ const NOTEBOOK_FIND_IN_MARKUP_PREVIEW = nls.localize('notebook.find.filter.findI const NOTEBOOK_FIND_IN_CODE_INPUT = nls.localize('notebook.find.filter.findInCodeInput', "Code Cell Source"); const NOTEBOOK_FIND_IN_CODE_OUTPUT = nls.localize('notebook.find.filter.findInCodeOutput', "Code Cell Output"); -const NOTEBOOK_FIND_WIDGET_INITIAL_WIDTH = 318; +const NOTEBOOK_FIND_WIDGET_INITIAL_WIDTH = 419; const NOTEBOOK_FIND_WIDGET_INITIAL_HORIZONTAL_PADDING = 4; class NotebookFindFilterActionViewItem extends DropdownMenuActionViewItem { constructor(readonly filters: NotebookFindFilters, action: IAction, options: IActionViewItemOptions, actionRunner: IActionRunner, @IContextMenuService contextMenuService: IContextMenuService) { @@ -314,6 +319,10 @@ export abstract class SimpleFindReplaceWidget extends Widget { private _filters: NotebookFindFilters; + private readonly inSelectionToggle: Toggle; + private cellSelectionDecorationIds: string[] = []; + private textSelectionDecorationIds: ICellModelDecorations[] = []; + constructor( @IContextViewService private readonly _contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService, @@ -326,14 +335,14 @@ export abstract class SimpleFindReplaceWidget extends Widget { ) { super(); - const findScope = this._configurationService.getValue<{ + const findFilters = this._configurationService.getValue<{ markupSource: boolean; markupPreview: boolean; codeSource: boolean; codeOutput: boolean; - }>(NotebookSetting.findScope) ?? { markupSource: true, markupPreview: true, codeSource: true, codeOutput: true }; + }>(NotebookSetting.findFilters) ?? { markupSource: true, markupPreview: true, codeSource: true, codeOutput: true }; - this._filters = new NotebookFindFilters(findScope.markupSource, findScope.markupPreview, findScope.codeSource, findScope.codeOutput); + this._filters = new NotebookFindFilters(findFilters.markupSource, findFilters.markupPreview, findFilters.codeSource, findFilters.codeOutput, { findScopeType: NotebookFindScopeType.None }); this._state.change({ filters: this._filters }, false); this._filters.onDidChange(() => { @@ -378,6 +387,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { null, this._contextViewService, { + // width:FIND_INPUT_AREA_WIDTH, label: NLS_FIND_INPUT_LABEL, placeholder: NLS_FIND_INPUT_PLACEHOLDER, validation: (value: string): InputBoxMessage | null => { @@ -430,7 +440,6 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._findInput.setWholeWords(this._state.wholeWord); this._findInput.setCaseSensitive(this._state.matchCase); this._replaceInput.setPreserveCase(this._state.preserveCase); - this.findFirst(); })); this._matchesCount = document.createElement('div'); @@ -453,6 +462,59 @@ export abstract class SimpleFindReplaceWidget extends Widget { } }, hoverService)); + this.inSelectionToggle = this._register(new Toggle({ + icon: findSelectionIcon, + title: NLS_TOGGLE_SELECTION_FIND_TITLE, + isChecked: false, + inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground), + inputActiveOptionBorder: asCssVariable(inputActiveOptionBorder), + inputActiveOptionForeground: asCssVariable(inputActiveOptionForeground), + })); + this.inSelectionToggle.domNode.style.display = 'inline'; + + this.inSelectionToggle.onChange(() => { + const checked = this.inSelectionToggle.checked; + if (checked) { + // selection logic: + // 1. if there are multiple cells, do that. + // 2. if there is only one cell, do the following: + // - if there is a multi-line range highlighted, textual in selection + // - if there is no range, cell in selection for that cell + + const cellSelection: ICellRange[] = this._notebookEditor.getSelections(); + const textSelection: Range[] = this._notebookEditor.getSelectionViewModels()[0].getSelections(); + + if (cellSelection.length > 1 || cellSelection.some(range => range.end - range.start > 1)) { + this._filters.findScope = { + findScopeType: NotebookFindScopeType.Cells, + selectedCellRanges: cellSelection + }; + this.setCellSelectionDecorations(); + + } else if (textSelection.length > 1 || textSelection.some(range => range.endLineNumber - range.startLineNumber >= 1)) { + this._filters.findScope = { + findScopeType: NotebookFindScopeType.Text, + selectedCellRanges: cellSelection, + selectedTextRanges: textSelection + }; + this.setTextSelectionDecorations(textSelection, this._notebookEditor.getSelectionViewModels()[0]); + + } else { + this._filters.findScope = { + findScopeType: NotebookFindScopeType.Cells, + selectedCellRanges: cellSelection + }; + this.setCellSelectionDecorations(); + } + } else { + this._filters.findScope = { + findScopeType: NotebookFindScopeType.None + }; + this.clearCellSelectionDecorations(); + this.clearTextSelectionDecorations(); + } + }); + const closeBtn = this._register(new SimpleButton({ label: NLS_CLOSE_BTN_LABEL, icon: widgetClose, @@ -465,6 +527,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._innerFindDomNode.appendChild(this._matchesCount); this._innerFindDomNode.appendChild(this.prevBtn.domNode); this._innerFindDomNode.appendChild(this.nextBtn.domNode); + this._innerFindDomNode.appendChild(this.inSelectionToggle.domNode); this._innerFindDomNode.appendChild(closeBtn.domNode); // _domNode wraps _innerDomNode, ensuring that @@ -599,7 +662,6 @@ export abstract class SimpleFindReplaceWidget extends Widget { protected abstract onInputChanged(): boolean; protected abstract find(previous: boolean): void; - protected abstract findFirst(): void; protected abstract replaceOne(): void; protected abstract replaceAll(): void; protected abstract onFocusTrackerFocus(): void; @@ -647,15 +709,59 @@ export abstract class SimpleFindReplaceWidget extends Widget { this.updateButtons(this.foundMatch); } + private setCellSelectionDecorations() { + const cellHandles: number[] = []; + this._notebookEditor.getSelectionViewModels().forEach(viewModel => { + cellHandles.push(viewModel.handle); + }); + + const decorations: INotebookDeltaDecoration[] = []; + for (const handle of cellHandles) { + decorations.push({ + handle: handle, + options: { className: 'nb-multiCellHighlight', outputClassName: 'nb-multiCellHighlight' } + } satisfies INotebookDeltaDecoration); + } + this.cellSelectionDecorationIds = this._notebookEditor.deltaCellDecorations([], decorations); + } + + private clearCellSelectionDecorations() { + this._notebookEditor.deltaCellDecorations(this.cellSelectionDecorationIds, []); + } + + private setTextSelectionDecorations(textRanges: Range[], cell: ICellViewModel) { + this._notebookEditor.changeModelDecorations(changeAccessor => { + const decorations: ICellModelDeltaDecorations[] = []; + for (const range of textRanges) { + decorations.push({ + ownerId: cell.handle, + decorations: [{ + range: range, + options: { + description: 'text search range for notebook search scope', + isWholeLine: true, + className: 'nb-findScope' + } + }] + }); + } + this.textSelectionDecorationIds = changeAccessor.deltaDecorations([], decorations); + }); + } + + private clearTextSelectionDecorations() { + this._notebookEditor.changeModelDecorations(changeAccessor => { + changeAccessor.deltaDecorations(this.textSelectionDecorationIds, []); + }); + } + protected _updateMatchesCount(): void { } override dispose() { super.dispose(); - if (this._domNode && this._domNode.parentElement) { - this._domNode.parentElement.removeChild(this._domNode); - } + this._domNode.remove(); } public getDomNode() { @@ -686,7 +792,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._findInput.focus(); } - public show(initialInput?: string, options?: { focus?: boolean }): void { + public show(initialInput?: string, options?: IShowNotebookFindWidgetOptions): void { if (initialInput) { this._findInput.setValue(initialInput); } @@ -738,6 +844,12 @@ export abstract class SimpleFindReplaceWidget extends Widget { public hide(): void { if (this._isVisible) { + this.inSelectionToggle.checked = false; + this._notebookEditor.deltaCellDecorations(this.cellSelectionDecorationIds, []); + this._notebookEditor.changeModelDecorations(changeAccessor => { + changeAccessor.deltaDecorations(this.textSelectionDecorationIds, []); + }); + this._domNode.classList.remove('visible-transition'); this._domNode.setAttribute('aria-hidden', 'true'); // Need to delay toggling visibility until after Transition, then visibility hidden - removes from tabIndex list diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts index 7c92a6b0576..15954bc2b81 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts @@ -25,6 +25,7 @@ import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contr import { FindModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; import { SimpleFindReplaceWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget'; import { CellEditState, ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookFindScope } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; @@ -39,6 +40,7 @@ export interface IShowNotebookFindWidgetOptions { matchIndex?: number; focus?: boolean; searchStringSeededFrom?: { cell: ICellViewModel; range: Range }; + findScope?: INotebookFindScope; } export class NotebookFindContrib extends Disposable implements INotebookEditorContribution { @@ -118,7 +120,7 @@ class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEdi })); this._register(DOM.addDisposableListener(this.getDomNode(), DOM.EventType.FOCUS, e => { - this._previousFocusElement = e.relatedTarget instanceof HTMLElement ? e.relatedTarget : undefined; + this._previousFocusElement = DOM.isHTMLElement(e.relatedTarget) ? e.relatedTarget : undefined; }, true)); } @@ -345,9 +347,7 @@ class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEdi this._matchesCount.title = ''; // remove previous content - if (this._matchesCount.firstChild) { - this._matchesCount.removeChild(this._matchesCount.firstChild); - } + this._matchesCount.firstChild?.remove(); let label: string; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts index 166de405964..943e52e2c17 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -32,6 +32,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { CodeActionParticipantUtils } from 'vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants'; // format notebook registerAction2(class extends Action2 { @@ -63,6 +64,7 @@ registerAction2(class extends Action2 { const editorWorkerService = accessor.get(IEditorWorkerService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); const bulkEditService = accessor.get(IBulkEditService); + const instantiationService = accessor.get(IInstantiationService); const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); if (!editor || !editor.hasModel()) { @@ -70,37 +72,41 @@ registerAction2(class extends Action2 { } const notebook = editor.textModel; + + const formatApplied: boolean = await instantiationService.invokeFunction(CodeActionParticipantUtils.checkAndRunFormatCodeAction, notebook, Progress.None, CancellationToken.None); + const disposable = new DisposableStore(); try { - const allCellEdits = await Promise.all(notebook.cells.map(async cell => { - const ref = await textModelService.createModelReference(cell.uri); - disposable.add(ref); + if (!formatApplied) { + const allCellEdits = await Promise.all(notebook.cells.map(async cell => { + const ref = await textModelService.createModelReference(cell.uri); + disposable.add(ref); - const model = ref.object.textEditorModel; + const model = ref.object.textEditorModel; - const formatEdits = await getDocumentFormattingEditsWithSelectedProvider( - editorWorkerService, - languageFeaturesService, - model, - FormattingMode.Explicit, - CancellationToken.None - ); + const formatEdits = await getDocumentFormattingEditsWithSelectedProvider( + editorWorkerService, + languageFeaturesService, + model, + FormattingMode.Explicit, + CancellationToken.None + ); - const edits: ResourceTextEdit[] = []; + const edits: ResourceTextEdit[] = []; - if (formatEdits) { - for (const edit of formatEdits) { - edits.push(new ResourceTextEdit(model.uri, edit, model.getVersionId())); + if (formatEdits) { + for (const edit of formatEdits) { + edits.push(new ResourceTextEdit(model.uri, edit, model.getVersionId())); + } + + return edits; } - return edits; - } - - return []; - })); - - await bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('label', "Format Notebook"), code: 'undoredo.formatNotebook', }); + return []; + })); + await bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('label', "Format Notebook"), code: 'undoredo.formatNotebook', }); + } } finally { disposable.dispose(); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts index e6656eaac73..203839dfd85 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts @@ -22,7 +22,7 @@ import { CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION } from 'vs/workbench/contrib/not import { INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, findTargetCellEditor } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CURSOR_NAVIGATION_MODE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_INPUT_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CURSOR_NAVIGATION_MODE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_INPUT_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_CELL_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; const NOTEBOOK_FOCUS_TOP = 'notebook.focusTop'; const NOTEBOOK_FOCUS_BOTTOM = 'notebook.focusBottom'; @@ -104,10 +104,10 @@ registerAction2(class FocusNextCellAction extends NotebookCellAction { weight: KeybindingWeight.WorkbenchContrib }, { - when: NOTEBOOK_EDITOR_FOCUSED, - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, }, - weight: KeybindingWeight.WorkbenchContrib + when: ContextKeyExpr.and(NOTEBOOK_CELL_EDITOR_FOCUSED, CONTEXT_ACCESSIBILITY_MODE_ENABLED), + primary: KeyMod.CtrlCmd | KeyCode.PageDown, + mac: { primary: KeyMod.WinCtrl | KeyCode.PageUp, }, + weight: KeybindingWeight.WorkbenchContrib + 1 }, ] }); @@ -180,10 +180,10 @@ registerAction2(class FocusPreviousCellAction extends NotebookCellAction { weight: KeybindingWeight.WorkbenchContrib, // markdown keybinding, focus on list: higher weight to override list.focusDown }, { - when: NOTEBOOK_EDITOR_FOCUSED, - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp }, - weight: KeybindingWeight.WorkbenchContrib + when: ContextKeyExpr.and(NOTEBOOK_CELL_EDITOR_FOCUSED, CONTEXT_ACCESSIBILITY_MODE_ENABLED), + primary: KeyMod.CtrlCmd | KeyCode.PageUp, + mac: { primary: KeyMod.WinCtrl | KeyCode.PageUp, }, + weight: KeybindingWeight.WorkbenchContrib + 1 }, ], }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts index c05cdaae7f6..5ca97b08f95 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts @@ -128,9 +128,8 @@ export class NotebookVariablesView extends ViewPane { [CONTEXT_VARIABLE_LANGUAGE.key, element.language], [CONTEXT_VARIABLE_EXTENSIONID.key, element.extensionId] ]); - const menu = this.menuService.createMenu(MenuId.NotebookVariablesContext, overlayedContext); - createAndFillInContextMenuActions(menu, { arg, shouldForwardArgs: true }, actions); - menu.dispose(); + const menu = this.menuService.getMenuActions(MenuId.NotebookVariablesContext, overlayedContext, { arg, shouldForwardArgs: true }); + createAndFillInContextMenuActions(menu, actions); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => actions diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 1a0f19d4a58..c9d3c97a772 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -29,8 +29,8 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { IEditorPane } from 'vs/workbench/common/editor'; import { CellFoldingState, CellRevealType, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, INotebookEditor, INotebookEditorOptions, INotebookEditorPane, INotebookViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; -import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; -import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookCellOutlineDataSource, NotebookCellOutlineDataSource } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource'; +import { CellKind, NotebookCellsChangeType, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IBreadcrumbsDataSource, IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigCollapseItemsValues, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; @@ -46,12 +46,14 @@ import { MenuEntryActionViewItem, createAndFillInActionBarActions } from 'vs/pla import { IAction } from 'vs/base/common/actions'; import { NotebookSectionArgs } from 'vs/workbench/contrib/notebook/browser/controller/sectionActions'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; -import { disposableTimeout } from 'vs/base/common/async'; +import { Delayer, disposableTimeout } from 'vs/base/common/async'; import { IOutlinePane } from 'vs/workbench/contrib/outline/browser/outline'; import { Codicon } from 'vs/base/common/codicons'; import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { NotebookOutlineConstants } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; -import { INotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory'; +import { INotebookCellOutlineDataSourceFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory'; +import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; class NotebookOutlineTemplate { @@ -286,33 +288,44 @@ class NotebookOutlineVirtualDelegate implements IListVirtualDelegate { + private readonly _disposables = new DisposableStore(); + + private gotoShowCodeCellSymbols: boolean; + constructor( - private _getEntries: () => OutlineEntry[], + private readonly notebookCellOutlineDataSourceRef: IReference | undefined, @IConfigurationService private readonly _configurationService: IConfigurationService, @IThemeService private readonly _themeService: IThemeService - ) { } + ) { + this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.gotoSymbolsAllSymbols)) { + this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + } + })); + } getQuickPickElements(): IQuickPickOutlineElement[] { const bucket: OutlineEntry[] = []; - for (const entry of this._getEntries()) { + for (const entry of this.notebookCellOutlineDataSourceRef?.object?.entries ?? []) { entry.asFlatList(bucket); } const result: IQuickPickOutlineElement[] = []; const { hasFileIcons } = this._themeService.getFileIconTheme(); - const showSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); const isSymbol = (element: OutlineEntry) => !!element.symbolKind; const isCodeCell = (element: OutlineEntry) => (element.cell.cellKind === CellKind.Code && element.level === NotebookOutlineConstants.NonHeaderOutlineLevel); // code cell entries are exactly level 7 by this constant for (let i = 0; i < bucket.length; i++) { const element = bucket[i]; const nextElement = bucket[i + 1]; // can be undefined - if (!showSymbols + if (!this.gotoShowCodeCellSymbols && isSymbol(element)) { continue; } - if (showSymbols + if (this.gotoShowCodeCellSymbols && isCodeCell(element) && nextElement && isSymbol(nextElement)) { continue; @@ -330,32 +343,98 @@ export class NotebookQuickPickProvider implements IQuickPickDataSource { + + private readonly _disposables = new DisposableStore(); + + private showCodeCells: boolean; + private showCodeCellSymbols: boolean; + private showMarkdownHeadersOnly: boolean; + constructor( - private _getEntries: () => OutlineEntry[], + private readonly outlineDataSourceRef: IReference | undefined, @IConfigurationService private readonly _configurationService: IConfigurationService, - ) { } + ) { + this.showCodeCells = this._configurationService.getValue(NotebookSetting.outlineShowCodeCells); + this.showCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); + this.showMarkdownHeadersOnly = this._configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); + + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.outlineShowCodeCells)) { + this.showCodeCells = this._configurationService.getValue(NotebookSetting.outlineShowCodeCells); + } + if (e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols)) { + this.showCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); + } + if (e.affectsConfiguration(NotebookSetting.outlineShowMarkdownHeadersOnly)) { + this.showMarkdownHeadersOnly = this._configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); + } + })); + } + + public getActiveEntry(): OutlineEntry | undefined { + const newActive = this.outlineDataSourceRef?.object?.activeElement; + if (!newActive) { + return undefined; + } + + if (!this.filterEntry(newActive)) { + return newActive; + } + + // find a valid parent + let parent = newActive.parent; + while (parent) { + if (this.filterEntry(parent)) { + parent = parent.parent; + } else { + return parent; + } + } + + // no valid parent found, return undefined + return undefined; + } + + /** + * Checks if the given outline entry should be filtered out of the outlinePane + * + * @param entry the OutlineEntry to check + * @returns true if the entry should be filtered out of the outlinePane + */ + private filterEntry(entry: OutlineEntry): boolean { + // if any are true, return true, this entry should NOT be included in the outline + if ( + (this.showMarkdownHeadersOnly && entry.cell.cellKind === CellKind.Markup && entry.level === NotebookOutlineConstants.NonHeaderOutlineLevel) || // show headers only + cell is mkdn + is level 7 (not header) + (!this.showCodeCells && entry.cell.cellKind === CellKind.Code) || // show code cells off + cell is code + (!this.showCodeCellSymbols && entry.cell.cellKind === CellKind.Code && entry.level > NotebookOutlineConstants.NonHeaderOutlineLevel) // show symbols off + cell is code + is level >7 (nb symbol levels) + ) { + return true; + } + + return false; + } *getChildren(element: NotebookCellOutline | OutlineEntry): Iterable { - const showCodeCells = this._configurationService.getValue(NotebookSetting.outlineShowCodeCells); - const showCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); - const showMarkdownHeadersOnly = this._configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); - const isOutline = element instanceof NotebookCellOutline; - const entries = isOutline ? this._getEntries() : element.children; + const entries = isOutline ? this.outlineDataSourceRef?.object?.entries ?? [] : element.children; for (const entry of entries) { if (entry.cell.cellKind === CellKind.Markup) { - if (!showMarkdownHeadersOnly) { + if (!this.showMarkdownHeadersOnly) { yield entry; } else if (entry.level < NotebookOutlineConstants.NonHeaderOutlineLevel) { yield entry; } - } else if (showCodeCells && entry.cell.cellKind === CellKind.Code) { - if (showCodeCellSymbols) { + } else if (this.showCodeCells && entry.cell.cellKind === CellKind.Code) { + if (this.showCodeCellSymbols) { yield entry; } else if (entry.level === NotebookOutlineConstants.NonHeaderOutlineLevel) { yield entry; @@ -363,26 +442,45 @@ export class NotebookOutlinePaneProvider implements IDataSource { + + private readonly _disposables = new DisposableStore(); + + private showCodeCells: boolean; + constructor( - private _getActiveElement: () => OutlineEntry | undefined, + private readonly outlineDataSourceRef: IReference | undefined, @IConfigurationService private readonly _configurationService: IConfigurationService, - ) { } + ) { + this.showCodeCells = this._configurationService.getValue(NotebookSetting.breadcrumbsShowCodeCells); + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.breadcrumbsShowCodeCells)) { + this.showCodeCells = this._configurationService.getValue(NotebookSetting.breadcrumbsShowCodeCells); + } + })); + } getBreadcrumbElements(): readonly OutlineEntry[] { const result: OutlineEntry[] = []; - const showCodeCells = this._configurationService.getValue(NotebookSetting.breadcrumbsShowCodeCells); - let candidate = this._getActiveElement(); + let candidate = this.outlineDataSourceRef?.object?.activeElement; while (candidate) { - if (showCodeCells || candidate.cell.cellKind !== CellKind.Code) { + if (this.showCodeCells || candidate.cell.cellKind !== CellKind.Code) { result.unshift(candidate); } candidate = candidate.parent; } return result; } + + dispose(): void { + this._disposables.dispose(); + } } class NotebookComparator implements IOutlineComparator { @@ -401,64 +499,80 @@ class NotebookComparator implements IOutlineComparator { } export class NotebookCellOutline implements IOutline { - - private readonly _dispoables = new DisposableStore(); - - private readonly _onDidChange = new Emitter(); - - readonly onDidChange: Event = this._onDidChange.event; - - get entries(): OutlineEntry[] { - return this._outlineProviderReference?.object?.entries ?? []; - } - - private readonly _entriesDisposables = new DisposableStore(); - - readonly config: IOutlineListConfig; - readonly outlineKind = 'notebookCells'; + private readonly _disposables = new DisposableStore(); + private readonly _modelDisposables = new DisposableStore(); + private readonly _dataSourceDisposables = new DisposableStore(); + + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + private readonly delayerRecomputeState: Delayer = this._disposables.add(new Delayer(300)); + private readonly delayerRecomputeActive: Delayer = this._disposables.add(new Delayer(200)); + // this can be long, because it will force a recompute at the end, so ideally we only do this once all nb language features are registered + private readonly delayerRecomputeSymbols: Delayer = this._disposables.add(new Delayer(2000)); + + readonly config: IOutlineListConfig; + private _outlineDataSourceReference: IReference | undefined; + // These three fields will always be set via setDataSources() on L475 + private _treeDataSource!: IDataSource; + private _quickPickDataSource!: IQuickPickDataSource; + private _breadcrumbsDataSource!: IBreadcrumbsDataSource; + + // view settings + private gotoShowCodeCellSymbols: boolean; + private outlineShowCodeCellSymbols: boolean; + + // getters get activeElement(): OutlineEntry | undefined { - return this._outlineProviderReference?.object?.activeElement; + this.checkDelayer(); + if (this._target === OutlineTarget.OutlinePane) { + return (this.config.treeDataSource as NotebookOutlinePaneProvider).getActiveEntry(); + } else { + console.error('activeElement should not be called outside of the OutlinePane'); + return undefined; + } + } + get entries(): OutlineEntry[] { + this.checkDelayer(); + return this._outlineDataSourceReference?.object?.entries ?? []; + } + get uri(): URI | undefined { + return this._outlineDataSourceReference?.object?.uri; + } + get isEmpty(): boolean { + return this._outlineDataSourceReference?.object?.isEmpty ?? true; } - private _outlineProviderReference: IReference | undefined; - private readonly _localDisposables = new DisposableStore(); + private checkDelayer() { + if (this.delayerRecomputeState.isTriggered()) { + this.delayerRecomputeState.cancel(); + this.recomputeState(); + } + } constructor( private readonly _editor: INotebookEditorPane, - _target: OutlineTarget, - @IInstantiationService instantiationService: IInstantiationService, + private readonly _target: OutlineTarget, + @IThemeService private readonly _themeService: IThemeService, @IEditorService private readonly _editorService: IEditorService, - @IConfigurationService _configurationService: IConfigurationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, ) { - const installSelectionListener = () => { - const notebookEditor = _editor.getControl(); - if (!notebookEditor?.hasModel()) { - this._outlineProviderReference?.dispose(); - this._outlineProviderReference = undefined; - this._localDisposables.clear(); - } else { - this._outlineProviderReference?.dispose(); - this._localDisposables.clear(); - this._outlineProviderReference = instantiationService.invokeFunction((accessor) => accessor.get(INotebookCellOutlineProviderFactory).getOrCreate(notebookEditor, _target)); - this._localDisposables.add(this._outlineProviderReference.object.onDidChange(e => { - this._onDidChange.fire(e); - })); - } - }; + this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + this.outlineShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); - this._dispoables.add(_editor.onDidChangeModel(() => { - installSelectionListener(); - })); + this.initializeOutline(); - installSelectionListener(); const delegate = new NotebookOutlineVirtualDelegate(); - const renderers = [instantiationService.createInstance(NotebookOutlineRenderer, this._editor.getControl(), _target)]; + const renderers = [this._instantiationService.createInstance(NotebookOutlineRenderer, this._editor.getControl(), this._target)]; const comparator = new NotebookComparator(); const options: IWorkbenchDataTreeOptions = { - collapseByDefault: _target === OutlineTarget.Breadcrumbs || (_target === OutlineTarget.OutlinePane && _configurationService.getValue(OutlineConfigKeys.collapseItems) === OutlineConfigCollapseItemsValues.Collapsed), + collapseByDefault: this._target === OutlineTarget.Breadcrumbs || (this._target === OutlineTarget.OutlinePane && this._configurationService.getValue(OutlineConfigKeys.collapseItems) === OutlineConfigCollapseItemsValues.Collapsed), expandOnlyOnTwistieClick: true, multipleSelectionSupport: false, accessibilityProvider: new NotebookOutlineAccessibility(), @@ -467,9 +581,9 @@ export class NotebookCellOutline implements IOutline { }; this.config = { - treeDataSource: instantiationService.createInstance(NotebookOutlinePaneProvider, () => (this.entries ?? [])), - quickPickDataSource: instantiationService.createInstance(NotebookQuickPickProvider, () => (this.entries ?? [])), - breadcrumbsDataSource: instantiationService.createInstance(NotebookBreadcrumbsProvider, () => (this.activeElement)), + treeDataSource: this._treeDataSource, + quickPickDataSource: this._quickPickDataSource, + breadcrumbsDataSource: this._breadcrumbsDataSource, delegate, renderers, comparator, @@ -477,25 +591,150 @@ export class NotebookCellOutline implements IOutline { }; } - async setFullSymbols(cancelToken: CancellationToken) { - await this._outlineProviderReference?.object?.setFullSymbols(cancelToken); + private initializeOutline() { + // initial setup + this.setDataSources(); + this.setModelListeners(); + + // reset the data sources + model listeners when we get a new notebook model + this._disposables.add(this._editor.onDidChangeModel(() => { + this.setDataSources(); + this.setModelListeners(); + this.computeSymbols(); + })); + + // recompute symbols as document symbol providers are updated in the language features registry + this._disposables.add(this._languageFeaturesService.documentSymbolProvider.onDidChange(() => { + this.delayedComputeSymbols(); + })); + + // recompute active when the selection changes + this._disposables.add(this._editor.onDidChangeSelection(() => { + this.delayedRecomputeActive(); + })); + + // recompute state when filter config changes + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.outlineShowMarkdownHeadersOnly) || + e.affectsConfiguration(NotebookSetting.outlineShowCodeCells) || + e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols) || + e.affectsConfiguration(NotebookSetting.breadcrumbsShowCodeCells) + ) { + this.delayedRecomputeState(); + } + })); + + // recompute state when execution states change + this._disposables.add(this._notebookExecutionStateService.onDidChangeExecution(e => { + if (e.type === NotebookExecutionType.cell && !!this._editor.textModel && e.affectsNotebook(this._editor.textModel?.uri)) { + this.delayedRecomputeState(); + } + })); + + // recompute symbols when the configuration changes (recompute state - and therefore recompute active - is also called within compute symbols) + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.gotoSymbolsAllSymbols) || e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols)) { + this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + this.outlineShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); + this.computeSymbols(); + } + })); + + // fire a change event when the theme changes + this._disposables.add(this._themeService.onDidFileIconThemeChange(() => { + this._onDidChange.fire({}); + })); + + // finish with a recompute state + this.recomputeState(); } - get uri(): URI | undefined { - return this._outlineProviderReference?.object?.uri; + /** + * set up the primary data source + three viewing sources for the various outline views + */ + private setDataSources(): void { + const notebookEditor = this._editor.getControl(); + this._outlineDataSourceReference?.dispose(); + this._dataSourceDisposables.clear(); + + if (!notebookEditor?.hasModel()) { + this._outlineDataSourceReference = undefined; + } else { + this._outlineDataSourceReference = this._dataSourceDisposables.add(this._instantiationService.invokeFunction((accessor) => accessor.get(INotebookCellOutlineDataSourceFactory).getOrCreate(notebookEditor))); + // escalate outline data source change events + this._dataSourceDisposables.add(this._outlineDataSourceReference.object.onDidChange(() => { + this._onDidChange.fire({}); + })); + } + + // these fields can be passed undefined outlineDataSources. View Providers all handle it accordingly + this._treeDataSource = this._dataSourceDisposables.add(this._instantiationService.createInstance(NotebookOutlinePaneProvider, this._outlineDataSourceReference)); + this._quickPickDataSource = this._dataSourceDisposables.add(this._instantiationService.createInstance(NotebookQuickPickProvider, this._outlineDataSourceReference)); + this._breadcrumbsDataSource = this._dataSourceDisposables.add(this._instantiationService.createInstance(NotebookBreadcrumbsProvider, this._outlineDataSourceReference)); } - get isEmpty(): boolean { - return this._outlineProviderReference?.object?.isEmpty ?? true; + + /** + * set up the listeners for the outline content, these respond to model changes in the notebook + */ + private setModelListeners(): void { + this._modelDisposables.clear(); + if (!this._editor.textModel) { + return; + } + + // Perhaps this is the first time we're building the outline + if (!this.entries.length) { + this.computeSymbols(); + } + + // recompute state when there are notebook content changes + this._modelDisposables.add(this._editor.textModel.onDidChangeContent(contentChanges => { + if (contentChanges.rawEvents.some(c => + c.kind === NotebookCellsChangeType.ChangeCellContent || + c.kind === NotebookCellsChangeType.ChangeCellInternalMetadata || + c.kind === NotebookCellsChangeType.Move || + c.kind === NotebookCellsChangeType.ModelChange)) { + this.delayedRecomputeState(); + } + })); } + + private async computeSymbols(cancelToken: CancellationToken = CancellationToken.None) { + if (this._target === OutlineTarget.QuickPick && this.gotoShowCodeCellSymbols) { + await this._outlineDataSourceReference?.object?.computeFullSymbols(cancelToken); + } else if (this._target === OutlineTarget.OutlinePane && this.outlineShowCodeCellSymbols) { + // No need to wait for this, we want the outline to show up quickly. + void this._outlineDataSourceReference?.object?.computeFullSymbols(cancelToken); + } + } + private async delayedComputeSymbols() { + this.delayerRecomputeState.cancel(); + this.delayerRecomputeActive.cancel(); + this.delayerRecomputeSymbols.trigger(() => { this.computeSymbols(); }); + } + + private recomputeState() { this._outlineDataSourceReference?.object?.recomputeState(); } + private delayedRecomputeState() { + this.delayerRecomputeActive.cancel(); // Active is always recomputed after a recomputing the State. + this.delayerRecomputeState.trigger(() => { this.recomputeState(); }); + } + + private recomputeActive() { this._outlineDataSourceReference?.object?.recomputeActive(); } + private delayedRecomputeActive() { + this.delayerRecomputeActive.trigger(() => { this.recomputeActive(); }); + } + async reveal(entry: OutlineEntry, options: IEditorOptions, sideBySide: boolean): Promise { + const notebookEditorOptions: INotebookEditorOptions = { + ...options, + override: this._editor.input?.editorId, + cellRevealType: CellRevealType.NearTopIfOutsideViewport, + selection: entry.position, + viewState: undefined, + }; await this._editorService.openEditor({ resource: entry.cell.uri, - options: { - ...options, - override: this._editor.input?.editorId, - cellRevealType: CellRevealType.NearTopIfOutsideViewport, - selection: entry.position - } as INotebookEditorOptions, + options: notebookEditorOptions, }, sideBySide ? SIDE_GROUP : undefined); } @@ -562,10 +801,10 @@ export class NotebookCellOutline implements IOutline { dispose(): void { this._onDidChange.dispose(); - this._dispoables.dispose(); - this._entriesDisposables.dispose(); - this._outlineProviderReference?.dispose(); - this._localDisposables.dispose(); + this._disposables.dispose(); + this._modelDisposables.dispose(); + this._dataSourceDisposables.dispose(); + this._outlineDataSourceReference?.dispose(); } } @@ -576,7 +815,6 @@ export class NotebookOutlineCreator implements IOutlineCreator reg.dispose(); @@ -587,18 +825,7 @@ export class NotebookOutlineCreator implements IOutlineCreator | undefined> { - const outline = this._instantiationService.createInstance(NotebookCellOutline, editor, target); - - const showAllGotoSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); - const showAllOutlineSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); - if (target === OutlineTarget.QuickPick && showAllGotoSymbols) { - await outline.setFullSymbols(cancelToken); - } else if (target === OutlineTarget.OutlinePane && showAllOutlineSymbols) { - // No need to wait for this, we want the outline to show up quickly. - void outline.setFullSymbols(cancelToken); - } - - return outline; + return this._instantiationService.createInstance(NotebookCellOutline, editor, target); } } @@ -677,7 +904,6 @@ registerAction2(class ToggleShowMarkdownHeadersOnly extends Action2 { } }); - registerAction2(class ToggleCodeCellEntries extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts index 89b90bbff4b..d5899e31986 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts @@ -433,7 +433,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa } } -class CodeActionParticipantUtils { +export class CodeActionParticipantUtils { static async checkAndRunFormatCodeAction( accessor: ServicesAccessor, @@ -499,7 +499,7 @@ class CodeActionParticipantUtils { }; for (const codeActionKind of codeActionsOnSave) { - const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, languageFeaturesService, getActionProgress, token); + const actionsToRun = await CodeActionParticipantUtils.getActionsToRun(model, codeActionKind, excludes, languageFeaturesService, getActionProgress, token); if (token.isCancellationRequested) { actionsToRun.dispose(); return; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts index 690a05c19fb..22b0a5e3fe3 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts @@ -98,11 +98,11 @@ export class TroubleshootController extends Disposable implements INotebookEdito items.push({ handle: i, items: [ - { + { text: `index: ${i}`, alignment: CellStatusbarAlignment.Left, priority: Number.MAX_SAFE_INTEGER - } + } satisfies INotebookCellStatusBarItem ] }); } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts index 1d5d4405f06..e69da8b479d 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts @@ -64,7 +64,7 @@ CommandsRegistry.registerCommand('_resolveNotebookKernels', async (accessor, arg }[]> => { const notebookKernelService = accessor.get(INotebookKernelService); const uri = URI.revive(args.uri as UriComponents); - const kernels = notebookKernelService.getMatchingKernel({ uri, viewType: args.viewType }); + const kernels = notebookKernelService.getMatchingKernel({ uri, notebookType: args.viewType }); return kernels.all.map(provider => ({ id: provider.id, diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts index 0db8e6dd2b4..310a367f197 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts @@ -7,18 +7,48 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; import { INotebookOutputActionContext, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { NOTEBOOK_CELL_HAS_OUTPUTS } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS, NOTEBOOK_CELL_HAS_OUTPUTS } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { ILogService } from 'vs/platform/log/common/log'; import { copyCellOutput } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ICellOutputViewModel, ICellViewModel, INotebookEditor, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; export const COPY_OUTPUT_COMMAND_ID = 'notebook.cellOutput.copy'; +registerAction2(class ShowAllOutputsAction extends Action2 { + constructor() { + super({ + id: 'notebook.cellOuput.showEmptyOutputs', + title: localize('notebookActions.showAllOutput', "Show empty outputs"), + menu: { + id: MenuId.NotebookOutputToolbar, + when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS) + }, + f1: false, + category: NOTEBOOK_ACTIONS_CATEGORY + }); + } + + run(accessor: ServicesAccessor, context: INotebookOutputActionContext): void { + const cell = context.cell; + if (cell && cell.cellKind === CellKind.Code) { + + for (let i = 1; i < cell.outputsViewModels.length; i++) { + if (!cell.outputsViewModels[i].visible.get()) { + cell.outputsViewModels[i].setVisible(true, true); + (cell as CodeCellViewModel).updateOutputHeight(i, 1, 'command'); + } + } + } + } +}); + registerAction2(class CopyCellOutputAction extends Action2 { constructor() { super({ @@ -95,7 +125,7 @@ function getOutputViewModelFromId(outputId: string, notebookEditor: INotebookEdi if (notebookViewModel) { const codeCells = notebookViewModel.viewCells.filter(cell => cell.cellKind === CellKind.Code) as CodeCellViewModel[]; for (const cell of codeCells) { - const output = cell.outputsViewModels.find(output => output.model.outputId === outputId); + const output = cell.outputsViewModels.find(output => output.model.outputId === outputId || output.model.alternativeOutputId === outputId); if (output) { return output; } @@ -104,3 +134,45 @@ function getOutputViewModelFromId(outputId: string, notebookEditor: INotebookEdi return undefined; } + +export const OPEN_OUTPUT_COMMAND_ID = 'notebook.cellOutput.openInTextEditor'; + +registerAction2(class OpenCellOutputInEditorAction extends Action2 { + constructor() { + super({ + id: OPEN_OUTPUT_COMMAND_ID, + title: localize('notebookActions.openOutputInEditor', "Open Cell Output in Text Editor"), + f1: false, + category: NOTEBOOK_ACTIONS_CATEGORY, + icon: icons.copyIcon, + }); + } + + private getNoteboookEditor(editorService: IEditorService, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): INotebookEditor | undefined { + if (outputContext && 'notebookEditor' in outputContext) { + return outputContext.notebookEditor; + } + return getNotebookEditorFromEditorPane(editorService.activeEditorPane); + } + + async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise { + const notebookEditor = this.getNoteboookEditor(accessor.get(IEditorService), outputContext); + + if (!notebookEditor) { + return; + } + + let outputViewModel: ICellOutputViewModel | undefined; + if (outputContext && 'outputId' in outputContext && typeof outputContext.outputId === 'string') { + outputViewModel = getOutputViewModelFromId(outputContext.outputId, notebookEditor); + } else if (outputContext && 'outputViewModel' in outputContext) { + outputViewModel = outputContext.outputViewModel; + } + + const openerService = accessor.get(IOpenerService); + + if (outputViewModel?.model.outputId && notebookEditor.textModel?.uri) { + openerService.open(CellUri.generateCellOutputUri(notebookEditor.textModel.uri, outputViewModel.model.outputId)); + } + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts index 7289f19d20b..f474b4dfc6b 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -15,8 +15,8 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_HAS_AGENT, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController'; import { CELL_TITLE_CELL_GROUP_ID, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getEditorFromArgsOrActivePane } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { insertNewCell } from 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; @@ -24,7 +24,6 @@ import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBro import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_GENERATED_BY_CHAT, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; - registerAction2(class extends NotebookAction { constructor() { super( @@ -259,9 +258,9 @@ registerAction2(class extends NotebookAction { menu: [ { id: MENU_CELL_CHAT_WIDGET_STATUS, - group: 'inline', + group: '0_main', order: 0, - when: CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages), + when: CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.Messages), } ], f1: false @@ -287,7 +286,7 @@ registerAction2(class extends NotebookAction { }, menu: { id: MENU_CELL_CHAT_WIDGET_STATUS, - group: 'main', + group: '0_main', order: 1 }, f1: false @@ -367,7 +366,7 @@ registerAction2(class extends NotebookAction { NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), ContextKeyExpr.not(InputFocusedContextKey), - CTX_INLINE_CHAT_HAS_PROVIDER, + CTX_NOTEBOOK_CHAT_HAS_AGENT, ContextKeyExpr.or( ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true), ContextKeyExpr.equals(`config.${NotebookSetting.cellGenerate}`, true) @@ -384,7 +383,7 @@ registerAction2(class extends NotebookAction { order: -1, when: ContextKeyExpr.and( NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), - CTX_INLINE_CHAT_HAS_PROVIDER, + CTX_NOTEBOOK_CHAT_HAS_AGENT, ContextKeyExpr.or( ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true), ContextKeyExpr.equals(`config.${NotebookSetting.cellGenerate}`, true) @@ -459,7 +458,7 @@ registerAction2(class extends NotebookAction { order: -1, when: ContextKeyExpr.and( NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), - CTX_INLINE_CHAT_HAS_PROVIDER, + CTX_NOTEBOOK_CHAT_HAS_AGENT, ContextKeyExpr.or( ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true), ContextKeyExpr.equals(`config.${NotebookSetting.cellGenerate}`, true) @@ -488,7 +487,7 @@ MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'betweenCells'), ContextKeyExpr.notEquals('config.notebook.insertToolbarLocation', 'hidden'), - CTX_INLINE_CHAT_HAS_PROVIDER, + CTX_NOTEBOOK_CHAT_HAS_AGENT, ContextKeyExpr.or( ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true), ContextKeyExpr.equals(`config.${NotebookSetting.cellGenerate}`, true) @@ -633,7 +632,7 @@ registerAction2(class extends NotebookCellAction { order: 0, when: ContextKeyExpr.and( NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), - CTX_INLINE_CHAT_HAS_PROVIDER, + CTX_NOTEBOOK_CHAT_HAS_AGENT, NOTEBOOK_CELL_GENERATED_BY_CHAT, ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true) ) diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts index 995340fb191..d642cbdcda9 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts @@ -3,4 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Disposable } from 'vs/base/common/lifecycle'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import 'vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions'; +import { CTX_NOTEBOOK_CHAT_HAS_AGENT } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; + +class NotebookChatContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.notebookChatContribution'; + + private readonly _ctxHasProvider: IContextKey; + + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @IChatAgentService chatAgentService: IChatAgentService + ) { + super(); + + this._ctxHasProvider = CTX_NOTEBOOK_CHAT_HAS_AGENT.bindTo(contextKeyService); + + const updateNotebookAgentStatus = () => { + const hasNotebookAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Notebook)); + this._ctxHasProvider.set(hasNotebookAgent); + }; + + updateNotebookAgentStatus(); + this._register(chatAgentService.onDidChangeAgents(updateNotebookAgentStatus)); + } +} + +registerWorkbenchContribution2(NotebookChatContribution.ID, NotebookChatContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext.ts index 4c8a6aa024d..259b8e8303e 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext.ts @@ -17,3 +17,5 @@ export const MENU_CELL_CHAT_WIDGET = MenuId.for('cellChatWidget'); export const MENU_CELL_CHAT_WIDGET_STATUS = MenuId.for('cellChatWidget.status'); export const MENU_CELL_CHAT_WIDGET_FEEDBACK = MenuId.for('cellChatWidget.feedback'); export const MENU_CELL_CHAT_WIDGET_TOOLBAR = MenuId.for('cellChatWidget.toolbar'); + +export const CTX_NOTEBOOK_CHAT_HAS_AGENT = new RawContextKey('notebookChatAgentRegistered', false, localize('notebookChatAgentRegistered', "Whether a chat agent for notebook is registered")); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index e5c83779451..3de91da698d 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -26,19 +26,16 @@ import { ICursorStateComputer, ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IModelService } from 'vs/editor/common/services/model'; import { localize } from 'vs/nls'; -import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/utils'; -import { MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { insertCell, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; import { ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookViewZone } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -279,8 +276,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito @ILanguageService private readonly _languageService: ILanguageService, @INotebookExecutionStateService private _executionStateService: INotebookExecutionStateService, @IStorageService private readonly _storageService: IStorageService, - @IChatService private readonly _chatService: IChatService, - @IChatVariablesService private readonly _chatVariableService: IChatVariablesService, + @IChatService private readonly _chatService: IChatService ) { super(); this._ctxHasActiveRequest = CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.bindTo(this._contextKeyService); @@ -301,13 +297,6 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._historyCandidate = ''; this._storageService.store(NotebookChatController._storageKey, JSON.stringify(NotebookChatController._promptHistory), StorageScope.PROFILE, StorageTarget.USER); }; - - this._register(this._chatVariableService.registerVariable( - { id: '_notebookChatInput', name: '_notebookChatInput', description: '', hidden: true }, - async (_message, _arg, model) => { - return this._widget?.parentEditor.getModel()?.uri; - } - )); } private _registerFocusTracker() { @@ -422,16 +411,30 @@ export class NotebookChatController extends Disposable implements INotebookEdito const inlineChatWidget = this._widgetDisposableStore.add(this._instantiationService.createInstance( InlineChatWidget, - ChatAgentLocation.Notebook, { - telemetrySource: 'notebook-generate-cell', - inputMenuId: MenuId.ChatExecute, - widgetMenuId: MENU_INLINE_CHAT_WIDGET, + location: ChatAgentLocation.Notebook, + resolveData: () => { + const sessionInputUri = this.getSessionInputUri(); + if (!sessionInputUri) { + return undefined; + } + return { + type: ChatAgentLocation.Notebook, + sessionInputUri + }; + } + }, + { statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, - rendererOptions: { - renderTextEditsAsSummary: (uri) => { - return isEqual(uri, this._widget?.parentEditor.getModel()?.uri) - || isEqual(uri, this._notebookEditor.textModel?.uri); + chatWidgetViewOptions: { + rendererOptions: { + renderTextEditsAsSummary: (uri) => { + return isEqual(uri, this._widget?.parentEditor.getModel()?.uri) + || isEqual(uri, this._notebookEditor.textModel?.uri); + } + }, + menus: { + telemetrySource: 'notebook-generate-cell' } } } @@ -478,6 +481,10 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._sessionCtor = createCancelablePromise(async token => { await this._startSession(token); + assertType(this._model.value); + const model = this._model.value; + this._widget?.inlineChatWidget.setChatModel(model); + if (fakeParentEditor.hasModel()) { if (this._widget) { @@ -541,15 +548,20 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._widget.updateNotebookEditorFocusNSelections(); } + hasSession(chatModel: IChatModel) { + return this._model.value === chatModel; + } + + getSessionInputUri() { + return this._widget?.parentEditor.getModel()?.uri; + } + async acceptInput() { assertType(this._widget); await this._sessionCtor; assertType(this._model.value); assertType(this._strategy); - const model = this._model.value; - this._widget.inlineChatWidget.setChatModel(model); - const lastInput = this._widget.inlineChatWidget.value; this._historyUpdate(lastInput); @@ -665,7 +677,6 @@ export class NotebookChatController extends Disposable implements INotebookEdito store.dispose(); this._ctxHasActiveRequest.set(false); - this._widget.inlineChatWidget.updateProgress(false); this._widget.inlineChatWidget.updateInfo(''); this._widget.inlineChatWidget.updateToolbar(true); } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts index 3cc7faf8354..8ef65e7a121 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts @@ -13,7 +13,7 @@ import { getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, import { INTERACTIVE_WINDOW_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SOURCE_COUNT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { ICellRange, isICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorCommandsContext } from 'vs/workbench/common/editor'; +import { isEditorCommandsContext } from 'vs/workbench/common/editor'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; @@ -220,9 +220,6 @@ export abstract class NotebookMultiCellAction extends Action2 { private isCellToolbarContext(context?: unknown): context is INotebookCellToolbarActionContext { return !!context && !!(context as INotebookActionContext).notebookEditor && (context as any).$mid === MarshalledId.NotebookCellActionContext; } - private isEditorContext(context?: unknown): boolean { - return !!context && (context as IEditorCommandsContext).groupId !== undefined; - } /** * The action/command args are resolved in following order @@ -233,7 +230,7 @@ export abstract class NotebookMultiCellAction extends Action2 { async run(accessor: ServicesAccessor, ...additionalArgs: any[]): Promise { const context = additionalArgs[0]; const isFromCellToolbar = this.isCellToolbarContext(context); - const isFromEditorToolbar = this.isEditorContext(context); + const isFromEditorToolbar = isEditorCommandsContext(context); const from = isFromCellToolbar ? 'cellToolbar' : (isFromEditorToolbar ? 'editorToolbar' : 'other'); const telemetryService = accessor.get(ITelemetryService); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index c73973e433e..82b3f2b8eaf 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -28,7 +28,7 @@ import { CELL_TITLE_CELL_GROUP_ID, CELL_TITLE_OUTPUT_GROUP_ID, CellToolbarOrder, import { NotebookChangeTabDisplaySize, NotebookIndentUsingSpaces, NotebookIndentUsingTabs, NotebookIndentationToSpacesAction, NotebookIndentationToTabsAction } from 'vs/workbench/contrib/notebook/browser/controller/notebookIndentationActions'; import { CHANGE_CELL_LANGUAGE, CellEditState, DETECT_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellEditType, CellKind, ICellEditOperation, NotebookCellExecutionState, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_INPUT_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_INPUT_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_CELL_IS_FIRST_OUTPUT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; @@ -227,7 +227,7 @@ registerAction2(class ClearCellOutputsAction extends NotebookCellAction { }, { id: MenuId.NotebookOutputToolbar, - when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE) + when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_IS_FIRST_OUTPUT, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON) }, ], keybinding: { diff --git a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts index ce41420deba..2caea9d0c0d 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts @@ -17,7 +17,6 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { EditorsOrder } from 'vs/workbench/common/editor'; import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; -import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; @@ -105,13 +104,6 @@ async function runCell(editorGroupsService: IEditorGroupsService, context: INote if (!foundEditor) { return; } - - const controller = InlineChatController.get(foundEditor); - if (!controller) { - return; - } - - controller.createSnapshot(); } registerAction2(class RenderAllMarkdownCellsAction extends NotebookAction { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 59a4d27a8e2..263d08b0c14 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -1544,11 +1544,10 @@ export class ModifiedElement extends AbstractElementRenderer { } })); - const menu = this.menuService.createMenu(MenuId.NotebookDiffCellInputTitle, scopedContextKeyService); + const menu = this.menuService.getMenuActions(MenuId.NotebookDiffCellInputTitle, scopedContextKeyService, { shouldForwardArgs: true }); const actions: IAction[] = []; - createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions); + createAndFillInActionBarActions(menu, actions); this._toolbar.setActions(actions); - menu.dispose(); } private async _initializeSourceDiffEditor() { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts index 00c89e900fa..ea94a04d2ad 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts @@ -181,7 +181,7 @@ export class OutputElement extends Disposable { this.resizeListener.clear(); const element = this.domNode; if (element) { - element.parentElement?.removeChild(element); + element.remove(); this._notebookEditor.removeInset( this._diffElementViewModel, this._nestedCell, @@ -259,7 +259,7 @@ export class OutputContainer extends Disposable { // already removed removedKeys.push(key); // remove element from DOM - this._outputContainer.removeChild(value.domNode); + value.domNode.remove(); this._editor.removeInset(this._diffElementViewModel, this._nestedCellViewModel, key, this._diffSide); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index 29dc02777d3..dd98ad9d3a4 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -41,13 +41,11 @@ import { BackLayerWebView, INotebookDelegateForWebview } from 'vs/workbench/cont import { NotebookDiffEditorEventDispatcher, NotebookDiffLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; -import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { cellIndexesToRanges, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { NotebookDiffOverviewRuler } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffOverviewRuler'; import { registerZIndex, ZIndex } from 'vs/platform/layout/browser/zIndexRegistry'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; const $ = DOM.$; @@ -151,11 +149,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, - @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, - @ICodeEditorService codeEditorService: ICodeEditorService ) { super(NotebookTextDiffEditor.ID, group, telemetryService, themeService, storageService); - this._notebookOptions = new NotebookOptions(this.window, this.configurationService, notebookExecutionStateService, codeEditorService, false); + this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, false, undefined); this._register(this._notebookOptions); this._revealFirst = true; } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 93989cbc04f..d659b3cf277 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -446,6 +446,12 @@ background-color: var(--vscode-notebook-symbolHighlightBackground) !important; } +/** Cell Search Range selection highlight */ +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-multiCellHighlight .cell-focus-indicator, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row.nb-multiCellHighlight { + background-color: var(--vscode-notebook-symbolHighlightBackground) !important; +} + /** Cell focused editor border */ .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-editor-part:before { outline: solid 1px var(--vscode-notebook-focusedEditorBorder); diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css index 4577677c02a..68974e01dc1 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css @@ -188,14 +188,6 @@ user-select: text; } -.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage .message[state="cropped"] { - -webkit-line-clamp: var(--vscode-inline-chat-cropped, 3); -} - -.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage .message[state="expanded"] { - -webkit-line-clamp: var(--vscode-inline-chat-expanded, 10); -} - .monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .label A { color: var(--vscode-textLink-foreground); cursor: pointer; @@ -352,4 +344,3 @@ .monaco-workbench .notebookOverlay .cell-chat-part .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { opacity: 1; } - diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 67c09c27463..4e174318c11 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -57,7 +57,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/browser/services/notebookRendererMessagingServiceImpl'; import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; -import { INotebookCellOutlineProviderFactory, NotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory'; +import { INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory'; // Editor Controller import 'vs/workbench/contrib/notebook/browser/controller/coreActions'; @@ -187,8 +187,8 @@ class NotebookDiffEditorSerializer implements IEditorSerializer { } type SerializedNotebookEditorData = { resource: URI; preferredResource: URI; viewType: string; options?: NotebookEditorInputOptions }; class NotebookEditorSerializer implements IEditorSerializer { - canSerialize(): boolean { - return true; + canSerialize(input: EditorInput): boolean { + return input.typeId === NotebookEditorInput.ID; } serialize(input: EditorInput): string { assertType(input instanceof NotebookEditorInput); @@ -644,7 +644,7 @@ class SimpleNotebookWorkingCopyEditorHandler extends Disposable implements IWork private handlesSync(workingCopy: IWorkingCopyIdentifier): string /* viewType */ | undefined { const viewType = this._getViewType(workingCopy); - if (!viewType || viewType === 'interactive') { + if (!viewType || viewType === 'interactive' || extname(workingCopy.resource) === '.replNotebook') { return undefined; } @@ -726,7 +726,7 @@ registerSingleton(INotebookExecutionStateService, NotebookExecutionStateService, registerSingleton(INotebookRendererMessagingService, NotebookRendererMessagingService, InstantiationType.Delayed); registerSingleton(INotebookKeymapService, NotebookKeymapService, InstantiationType.Delayed); registerSingleton(INotebookLoggingService, NotebookLoggingService, InstantiationType.Delayed); -registerSingleton(INotebookCellOutlineProviderFactory, NotebookCellOutlineProviderFactory, InstantiationType.Delayed); +registerSingleton(INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory, InstantiationType.Delayed); const schemas: IJSONSchemaMap = {}; function isConfigurationPropertySchema(x: IConfigurationPropertySchema | { [path: string]: IConfigurationPropertySchema }): x is IConfigurationPropertySchema { @@ -1026,8 +1026,8 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true }, - [NotebookSetting.findScope]: { - markdownDescription: nls.localize('notebook.findScope', "Customize the Find Widget behavior for searching within notebook cells. When both markup source and markup preview are enabled, the Find Widget will search either the source code or preview based on the current state of the cell."), + [NotebookSetting.findFilters]: { + markdownDescription: nls.localize('notebook.findFilters', "Customize the Find Widget behavior for searching within notebook cells. When both markup source and markup preview are enabled, the Find Widget will search either the source code or preview based on the current state of the cell."), type: 'object', properties: { markupSource: { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts index c06912b28f9..b146d0f71d2 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts @@ -28,6 +28,7 @@ export class NotebookAccessibilityHelp implements IAccessibleViewImplentation { } return; } + dispose() { } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts index 948db4cd3a1..33a38169f39 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts @@ -46,7 +46,7 @@ export class NotebookAccessibilityProvider extends Disposable implements IListAc getAriaLabel(element: CellViewModel) { const event = Event.filter(this.onDidAriaLabelChange, e => e === element); - return observableFromEvent(event, () => { + return observableFromEvent(this, event, () => { const viewModel = this.viewModel(); if (!viewModel) { return ''; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts index 3975c17bb9e..daa9bae804d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts @@ -20,6 +20,7 @@ export class NotebookAccessibleView implements IAccessibleViewImplentation { const editorService = accessor.get(IEditorService); return showAccessibleOutput(editorService); } + dispose() { } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index f474d202cb1..b0abb66c5fb 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -22,7 +22,7 @@ import { IEditorPane, IEditorPaneWithSelection } from 'vs/workbench/common/edito import { CellViewModelStateChangeEvent, NotebookCellStateChangedEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, ICellOutput, INotebookCellStatusBarItem, INotebookRendererInfo, INotebookSearchOptions, IOrderedMimeType, NotebookCellInternalMetadata, NotebookCellMetadata, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICellOutput, INotebookCellStatusBarItem, INotebookRendererInfo, INotebookFindOptions, IOrderedMimeType, NotebookCellInternalMetadata, NotebookCellMetadata, NOTEBOOK_EDITOR_ID, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { isCompositeNotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; @@ -31,6 +31,7 @@ import { IWebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IObservable } from 'vs/base/common/observable'; //#region Shared commands export const EXPAND_CELL_INPUT_COMMAND_ID = 'notebook.cell.expandCellInput'; @@ -108,6 +109,8 @@ export interface ICellOutputViewModel extends IDisposable { pickedMimeType: IOrderedMimeType | undefined; hasMultiMimeType(): boolean; readonly onDidResetRenderer: Event; + readonly visible: IObservable; + setVisible(visible: boolean, force?: boolean): void; resetRenderer(): void; toRawJSON(): any; } @@ -172,47 +175,47 @@ export enum CellLayoutState { Measured } -export interface CodeCellLayoutInfo { +/** LayoutInfo of the parts that are shared between all cell types. */ +export interface CellLayoutInfo { + readonly layoutState: CellLayoutState; readonly fontInfo: FontInfo | null; readonly chatHeight: number; - readonly editorHeight: number; readonly editorWidth: number; - readonly estimatedHasHorizontalScrolling: boolean; + readonly editorHeight: number; readonly statusBarHeight: number; + readonly commentOffset: number; readonly commentHeight: number; + readonly bottomToolbarOffset: number; readonly totalHeight: number; +} + +export interface CellLayoutChangeEvent { + readonly font?: FontInfo; + readonly outerWidth?: number; + readonly commentHeight?: boolean; +} + +export interface CodeCellLayoutInfo extends CellLayoutInfo { + readonly estimatedHasHorizontalScrolling: boolean; readonly outputContainerOffset: number; readonly outputTotalHeight: number; readonly outputShowMoreContainerHeight: number; readonly outputShowMoreContainerOffset: number; - readonly bottomToolbarOffset: number; - readonly layoutState: CellLayoutState; readonly codeIndicatorHeight: number; readonly outputIndicatorHeight: number; } -export interface CodeCellLayoutChangeEvent { +export interface CodeCellLayoutChangeEvent extends CellLayoutChangeEvent { readonly source?: string; readonly chatHeight?: boolean; readonly editorHeight?: boolean; - readonly commentHeight?: boolean; readonly outputHeight?: boolean; readonly outputShowMoreContainerHeight?: number; readonly totalHeight?: boolean; - readonly outerWidth?: number; - readonly font?: FontInfo; } -export interface MarkupCellLayoutInfo { - readonly fontInfo: FontInfo | null; - readonly chatHeight: number; - readonly editorWidth: number; - readonly editorHeight: number; - readonly statusBarHeight: number; +export interface MarkupCellLayoutInfo extends CellLayoutInfo { readonly previewHeight: number; - readonly bottomToolbarOffset: number; - readonly totalHeight: number; - readonly layoutState: CellLayoutState; readonly foldHintHeight: number; } @@ -220,9 +223,7 @@ export enum CellLayoutContext { Fold } -export interface MarkupCellLayoutChangeEvent { - readonly font?: FontInfo; - readonly outerWidth?: number; +export interface MarkupCellLayoutChangeEvent extends CellLayoutChangeEvent { readonly editorHeight?: number; readonly previewHeight?: number; totalHeight?: number; @@ -238,7 +239,7 @@ export interface ICellViewModel extends IGenericCellViewModel { readonly model: NotebookCellTextModel; readonly id: string; readonly textBuffer: IReadonlyTextBuffer; - readonly layoutInfo: { totalHeight: number; bottomToolbarOffset: number; editorWidth: number; editorHeight: number; statusBarHeight: number; chatHeight: number }; + readonly layoutInfo: CellLayoutInfo; readonly onDidChangeLayout: Event; readonly onDidChangeCellStatusBarItems: Event; readonly onCellDecorationsChanged: Event<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }>; @@ -256,6 +257,7 @@ export interface ICellViewModel extends IGenericCellViewModel { cellKind: CellKind; lineNumbers: 'on' | 'off' | 'inherit'; chatHeight: number; + commentHeight: number; focusMode: CellFocusMode; focusedOutputId?: string | undefined; outputIsHovered: boolean; @@ -383,6 +385,7 @@ export interface INotebookEditorCreationOptions { }; readonly options?: NotebookOptions; readonly codeWindow?: CodeWindow; + readonly forRepl?: boolean; } export interface INotebookWebviewMessage { @@ -448,7 +451,7 @@ export interface INotebookViewCellsUpdateEvent { export interface INotebookViewModel { notebookDocument: NotebookTextModel; - viewCells: ICellViewModel[]; + readonly viewCells: ICellViewModel[]; layoutInfo: NotebookLayoutInfo | null; onDidChangeViewCells: Event; onDidChangeSelection: Event; @@ -737,7 +740,7 @@ export interface INotebookEditor { getCellIndex(cell: ICellViewModel): number | undefined; getNextVisibleCellIndex(index: number): number | undefined; getPreviousVisibleCellIndex(index: number): number | undefined; - find(query: string, options: INotebookSearchOptions, token: CancellationToken, skipWarmup?: boolean, shouldGetSearchPreviewInfo?: boolean, ownerID?: string): Promise; + find(query: string, options: INotebookFindOptions, token: CancellationToken, skipWarmup?: boolean, shouldGetSearchPreviewInfo?: boolean, ownerID?: string): Promise; findHighlightCurrent(matchIndex: number, ownerID?: string): Promise; findUnHighlightCurrent(matchIndex: number, ownerID?: string): Promise; findStop(ownerID?: string): void; @@ -878,7 +881,9 @@ export function getNotebookEditorFromEditorPane(editorPane?: IEditorPane): INote const input = editorPane.input; - if (input && isCompositeNotebookEditorInput(input)) { + const isInteractiveEditor = input && isCompositeNotebookEditorInput(input); + + if (isInteractiveEditor || editorPane.getId() === REPL_EDITOR_ID) { return (editorPane.getControl() as { notebookEditor: INotebookEditor | undefined } | undefined)?.notebookEditor; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 05156eebb72..072a2a70593 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -74,13 +74,13 @@ import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser import { NotebookOverviewRuler } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler'; import { ListTopCellToolbar } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellEditType, CellKind, INotebookSearchOptions, RENDERER_NOT_AVAILABLE, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, INotebookFindOptions, NotebookFindScopeType, RENDERER_NOT_AVAILABLE, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NOTEBOOK_CURSOR_NAVIGATION_MODE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_OUTPUT_INPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookOptions, OutputInnerContainerTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; -import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { cellRangesToIndexes, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; @@ -99,7 +99,6 @@ import { NotebookStickyScroll } from 'vs/workbench/contrib/notebook/browser/view import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { PixelRatio } from 'vs/base/browser/pixelRatio'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { PreventDefaultContextMenuItemsContextKeyName } from 'vs/workbench/contrib/webview/browser/webview.contribution'; import { NotebookAccessibilityProvider } from 'vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider'; @@ -272,6 +271,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD readonly isEmbedded: boolean; private _readOnly: boolean; + private readonly _inRepl: boolean; public readonly scopedContextKeyService: IContextKeyService; private readonly instantiationService: IInstantiationService; @@ -302,7 +302,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD @IEditorProgressService private editorProgressService: IEditorProgressService, @INotebookLoggingService private readonly logService: INotebookLoggingService, @IKeybindingService private readonly keybindingService: IKeybindingService, - @ICodeEditorService codeEditorService: ICodeEditorService ) { super(); @@ -310,8 +309,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this.isEmbedded = creationOptions.isEmbedded ?? false; this._readOnly = creationOptions.isReadOnly ?? false; + this._inRepl = creationOptions.forRepl ?? false; - this._notebookOptions = creationOptions.options ?? new NotebookOptions(this.creationOptions?.codeWindow ?? mainWindow, this.configurationService, notebookExecutionStateService, codeEditorService, this._readOnly); + this._overlayContainer = document.createElement('div'); + this.scopedContextKeyService = this._register(contextKeyService.createScoped(this._overlayContainer)); + this.instantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); + + this._notebookOptions = creationOptions.options ?? + this.instantiationService.createInstance(NotebookOptions, this.creationOptions?.codeWindow ?? mainWindow, this._readOnly, undefined); this._register(this._notebookOptions); const eventDispatcher = this._register(new NotebookEventDispatcher()); this._viewContext = new ViewContext( @@ -322,9 +327,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._onDidChangeCellState.fire(e); })); - this._overlayContainer = document.createElement('div'); - this.scopedContextKeyService = this._register(contextKeyService.createScoped(this._overlayContainer)); - this.instantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); this._register(_notebookService.onDidChangeOutputRenderers(() => { this._updateOutputRenderers(); @@ -835,6 +837,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } `); + styleSheets.push(` + .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-multiCellHighlight:has(+ .monaco-list-row.nb-multiCellHighlight) .cell-focus-indicator-bottom { + height: ${bottomToolbarGap + cellBottomMargin}px; + background-color: var(--vscode-notebook-symbolHighlightBackground) !important; + } + + .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row.nb-multiCellHighlight:has(+ .monaco-list-row.nb-multiCellHighlight) .cell-focus-indicator-bottom { + height: ${bottomToolbarGap + cellBottomMargin - 6}px; + background-color: var(--vscode-notebook-symbolHighlightBackground) !important; + } + `); + styleSheets.push(` .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .input-collapse-container .cell-collapse-preview { @@ -1423,7 +1437,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD private async _attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined, perf?: NotebookPerfMarks) { this._ensureWebview(this.getId(), textModel.viewType, textModel.uri); - this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._viewContext, this.getLayoutInfo(), { isReadOnly: this._readOnly }); + this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._viewContext, this.getLayoutInfo(), { isReadOnly: this._readOnly, inRepl: this._inRepl }); this._viewContext.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); this.notebookOptions.updateOptions(this._readOnly); @@ -1814,6 +1828,19 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return; } + const whenContainerStylesLoaded = this.layoutService.whenContainerStylesLoaded(DOM.getWindow(this.getDomNode())); + if (whenContainerStylesLoaded) { + // In floating windows, we need to ensure that the + // container is ready for us to compute certain + // layout related properties. + whenContainerStylesLoaded.then(() => this.layoutNotebook(dimension, shadowElement, position)); + } else { + this.layoutNotebook(dimension, shadowElement, position); + } + + } + + private layoutNotebook(dimension: DOM.Dimension, shadowElement?: HTMLElement, position?: DOM.IDomPosition) { if (shadowElement) { this.updateShadowElement(shadowElement, dimension, position); } @@ -2537,11 +2564,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } } - return Promise.all(requests); } - async find(query: string, options: INotebookSearchOptions, token: CancellationToken, skipWarmup: boolean = false, shouldGetSearchPreviewInfo = false, ownerID?: string): Promise { + async find(query: string, options: INotebookFindOptions, token: CancellationToken, skipWarmup: boolean = false, shouldGetSearchPreviewInfo = false, ownerID?: string): Promise { if (!this._notebookViewModel) { return []; } @@ -2552,7 +2578,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const findMatches = this._notebookViewModel.find(query, options).filter(match => match.length > 0); - if (!options.includeMarkupPreview && !options.includeOutput) { + if ((!options.includeMarkupPreview && !options.includeOutput) || options.findScope?.findScopeType === NotebookFindScopeType.Text) { this._webview?.findStop(ownerID); return findMatches; } @@ -2576,7 +2602,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return []; } - const webviewMatches = await this._webview.find(query, { caseSensitive: options.caseSensitive, wholeWord: options.wholeWord, includeMarkup: !!options.includeMarkupPreview, includeOutput: !!options.includeOutput, shouldGetSearchPreviewInfo, ownerID }); + let findIds: string[] = []; + if (options.findScope && options.findScope.findScopeType === NotebookFindScopeType.Cells && options.findScope.selectedCellRanges) { + const selectedIndexes = cellRangesToIndexes(options.findScope.selectedCellRanges); + findIds = selectedIndexes.map(index => this._notebookViewModel?.viewCells[index].id ?? ''); + } + + const webviewMatches = await this._webview.find(query, { caseSensitive: options.caseSensitive, wholeWord: options.wholeWord, includeMarkup: !!options.includeMarkupPreview, includeOutput: !!options.includeOutput, shouldGetSearchPreviewInfo, ownerID, findIds: findIds }); if (token.isCancellationRequested) { return []; @@ -3195,54 +3227,19 @@ export const notebookCellBorder = registerColor('notebook.cellBorderColor', { hcLight: PANEL_BORDER }, nls.localize('notebook.cellBorderColor', "The border color for notebook cells.")); -export const focusedEditorBorderColor = registerColor('notebook.focusedEditorBorder', { - light: focusBorder, - dark: focusBorder, - hcDark: focusBorder, - hcLight: focusBorder -}, nls.localize('notebook.focusedEditorBorder', "The color of the notebook cell editor border.")); +export const focusedEditorBorderColor = registerColor('notebook.focusedEditorBorder', focusBorder, nls.localize('notebook.focusedEditorBorder', "The color of the notebook cell editor border.")); -export const cellStatusIconSuccess = registerColor('notebookStatusSuccessIcon.foreground', { - light: debugIconStartForeground, - dark: debugIconStartForeground, - hcDark: debugIconStartForeground, - hcLight: debugIconStartForeground -}, nls.localize('notebookStatusSuccessIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); +export const cellStatusIconSuccess = registerColor('notebookStatusSuccessIcon.foreground', debugIconStartForeground, nls.localize('notebookStatusSuccessIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); -export const runningCellRulerDecorationColor = registerColor('notebookEditorOverviewRuler.runningCellForeground', { - light: debugIconStartForeground, - dark: debugIconStartForeground, - hcDark: debugIconStartForeground, - hcLight: debugIconStartForeground -}, nls.localize('notebookEditorOverviewRuler.runningCellForeground', "The color of the running cell decoration in the notebook editor overview ruler.")); +export const runningCellRulerDecorationColor = registerColor('notebookEditorOverviewRuler.runningCellForeground', debugIconStartForeground, nls.localize('notebookEditorOverviewRuler.runningCellForeground', "The color of the running cell decoration in the notebook editor overview ruler.")); -export const cellStatusIconError = registerColor('notebookStatusErrorIcon.foreground', { - light: errorForeground, - dark: errorForeground, - hcDark: errorForeground, - hcLight: errorForeground -}, nls.localize('notebookStatusErrorIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); +export const cellStatusIconError = registerColor('notebookStatusErrorIcon.foreground', errorForeground, nls.localize('notebookStatusErrorIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); -export const cellStatusIconRunning = registerColor('notebookStatusRunningIcon.foreground', { - light: foreground, - dark: foreground, - hcDark: foreground, - hcLight: foreground -}, nls.localize('notebookStatusRunningIcon.foreground', "The running icon color of notebook cells in the cell status bar.")); +export const cellStatusIconRunning = registerColor('notebookStatusRunningIcon.foreground', foreground, nls.localize('notebookStatusRunningIcon.foreground', "The running icon color of notebook cells in the cell status bar.")); -export const notebookOutputContainerBorderColor = registerColor('notebook.outputContainerBorderColor', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, nls.localize('notebook.outputContainerBorderColor', "The border color of the notebook output container.")); +export const notebookOutputContainerBorderColor = registerColor('notebook.outputContainerBorderColor', null, nls.localize('notebook.outputContainerBorderColor', "The border color of the notebook output container.")); -export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, nls.localize('notebook.outputContainerBackgroundColor', "The color of the notebook output container background.")); +export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', null, nls.localize('notebook.outputContainerBackgroundColor', "The color of the notebook output container background.")); // TODO@rebornix currently also used for toolbar border, if we keep all of this, pick a generic name export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeparator', { @@ -3252,12 +3249,7 @@ export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeparat hcLight: contrastBorder }, nls.localize('notebook.cellToolbarSeparator', "The color of the separator in the cell bottom toolbar")); -export const focusedCellBackground = registerColor('notebook.focusedCellBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, nls.localize('focusedCellBackground', "The background color of a cell when the cell is focused.")); +export const focusedCellBackground = registerColor('notebook.focusedCellBackground', null, nls.localize('focusedCellBackground', "The background color of a cell when the cell is focused.")); export const selectedCellBackground = registerColor('notebook.selectedCellBackground', { dark: listInactiveSelectionBackground, @@ -3288,19 +3280,9 @@ export const inactiveSelectedCellBorder = registerColor('notebook.inactiveSelect hcLight: focusBorder }, nls.localize('notebook.inactiveSelectedCellBorder', "The color of the cell's borders when multiple cells are selected.")); -export const focusedCellBorder = registerColor('notebook.focusedCellBorder', { - dark: focusBorder, - light: focusBorder, - hcDark: focusBorder, - hcLight: focusBorder -}, nls.localize('notebook.focusedCellBorder', "The color of the cell's focus indicator borders when the cell is focused.")); +export const focusedCellBorder = registerColor('notebook.focusedCellBorder', focusBorder, nls.localize('notebook.focusedCellBorder', "The color of the cell's focus indicator borders when the cell is focused.")); -export const inactiveFocusedCellBorder = registerColor('notebook.inactiveFocusedCellBorder', { - dark: notebookCellBorder, - light: notebookCellBorder, - hcDark: notebookCellBorder, - hcLight: notebookCellBorder -}, nls.localize('notebook.inactiveFocusedCellBorder', "The color of the cell's top and bottom border when a cell is focused while the primary focus is outside of the editor.")); +export const inactiveFocusedCellBorder = registerColor('notebook.inactiveFocusedCellBorder', notebookCellBorder, nls.localize('notebook.inactiveFocusedCellBorder', "The color of the cell's top and bottom border when a cell is focused while the primary focus is outside of the editor.")); export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemHoverBackground', { light: new Color(new RGBA(0, 0, 0, 0.08)), @@ -3309,33 +3291,13 @@ export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemH hcLight: new Color(new RGBA(0, 0, 0, 0.08)), }, nls.localize('notebook.cellStatusBarItemHoverBackground', "The background color of notebook cell status bar items.")); -export const cellInsertionIndicator = registerColor('notebook.cellInsertionIndicator', { - light: focusBorder, - dark: focusBorder, - hcDark: focusBorder, - hcLight: focusBorder -}, nls.localize('notebook.cellInsertionIndicator', "The color of the notebook cell insertion indicator.")); +export const cellInsertionIndicator = registerColor('notebook.cellInsertionIndicator', focusBorder, nls.localize('notebook.cellInsertionIndicator', "The color of the notebook cell insertion indicator.")); -export const listScrollbarSliderBackground = registerColor('notebookScrollbarSlider.background', { - dark: scrollbarSliderBackground, - light: scrollbarSliderBackground, - hcDark: scrollbarSliderBackground, - hcLight: scrollbarSliderBackground -}, nls.localize('notebookScrollbarSliderBackground', "Notebook scrollbar slider background color.")); +export const listScrollbarSliderBackground = registerColor('notebookScrollbarSlider.background', scrollbarSliderBackground, nls.localize('notebookScrollbarSliderBackground', "Notebook scrollbar slider background color.")); -export const listScrollbarSliderHoverBackground = registerColor('notebookScrollbarSlider.hoverBackground', { - dark: scrollbarSliderHoverBackground, - light: scrollbarSliderHoverBackground, - hcDark: scrollbarSliderHoverBackground, - hcLight: scrollbarSliderHoverBackground -}, nls.localize('notebookScrollbarSliderHoverBackground', "Notebook scrollbar slider background color when hovering.")); +export const listScrollbarSliderHoverBackground = registerColor('notebookScrollbarSlider.hoverBackground', scrollbarSliderHoverBackground, nls.localize('notebookScrollbarSliderHoverBackground', "Notebook scrollbar slider background color when hovering.")); -export const listScrollbarSliderActiveBackground = registerColor('notebookScrollbarSlider.activeBackground', { - dark: scrollbarSliderActiveBackground, - light: scrollbarSliderActiveBackground, - hcDark: scrollbarSliderActiveBackground, - hcLight: scrollbarSliderActiveBackground -}, nls.localize('notebookScrollbarSliderActiveBackground', "Notebook scrollbar slider background color when clicked on.")); +export const listScrollbarSliderActiveBackground = registerColor('notebookScrollbarSlider.activeBackground', scrollbarSliderActiveBackground, nls.localize('notebookScrollbarSliderActiveBackground', "Notebook scrollbar slider background color when clicked on.")); export const cellSymbolHighlight = registerColor('notebook.symbolHighlightBackground', { dark: Color.fromHex('#ffffff0b'), diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index 20ffe3867e8..63c68389080 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -9,6 +9,7 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; export const selectKernelIcon = registerIcon('notebook-kernel-select', Codicon.serverEnvironment, localize('selectKernelIcon', 'Configure icon to select a kernel in notebook editors.')); export const executeIcon = registerIcon('notebook-execute', Codicon.play, localize('executeIcon', 'Icon to execute in notebook editors.')); +export const configIcon = registerIcon('notebook-config', Codicon.gear, localize('configIcon', 'Icon to configure in notebook editors.')); export const executeAboveIcon = registerIcon('notebook-execute-above', Codicon.runAbove, localize('executeAboveIcon', 'Icon to execute above cells in notebook editors.')); export const executeBelowIcon = registerIcon('notebook-execute-below', Codicon.runBelow, localize('executeBelowIcon', 'Icon to execute below cells in notebook editors.')); export const stopIcon = registerIcon('notebook-stop', Codicon.primitiveSquare, localize('stopIcon', 'Icon to stop an execution in notebook editors.')); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts index 46ce122a004..c83be8c872f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts @@ -137,11 +137,11 @@ export class NotebookOptions extends Disposable { constructor( readonly targetWindow: CodeWindow, - private readonly configurationService: IConfigurationService, - private readonly notebookExecutionStateService: INotebookExecutionStateService, - private readonly codeEditorService: ICodeEditorService, private isReadonly: boolean, - private readonly overrides?: { cellToolbarInteraction: string; globalToolbar: boolean; stickyScrollEnabled: boolean; dragAndDropEnabled: boolean } + private readonly overrides: { cellToolbarInteraction: string; globalToolbar: boolean; stickyScrollEnabled: boolean; dragAndDropEnabled: boolean } | undefined, + @IConfigurationService private readonly configurationService: IConfigurationService, + @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, ) { super(); const showCellStatusBar = this.configurationService.getValue(NotebookSetting.showCellStatusBar); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts index 4be07da63b7..5b1360f0ca8 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts @@ -19,6 +19,8 @@ import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { InteractiveWindowOpen } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; export class NotebookEditorWidgetService implements INotebookEditorService { @@ -39,7 +41,8 @@ export class NotebookEditorWidgetService implements INotebookEditorService { constructor( @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { const groupListener = new Map(); @@ -182,9 +185,13 @@ export class NotebookEditorWidgetService implements INotebookEditorService { if (!value) { // NEW widget - const instantiationService = accessor.get(IInstantiationService); + const editorGroupContextKeyService = accessor.get(IContextKeyService); + const editorGroupEditorProgressService = accessor.get(IEditorProgressService); + const notebookInstantiationService = this.instantiationService.createChild(new ServiceCollection( + [IContextKeyService, editorGroupContextKeyService], + [IEditorProgressService, editorGroupEditorProgressService])); const ctorOptions = creationOptions ?? getDefaultNotebookCreationOptions(); - const widget = instantiationService.createInstance(NotebookEditorWidget, { + const widget = notebookInstantiationService.createInstance(NotebookEditorWidget, { ...ctorOptions, codeWindow: codeWindow ?? ctorOptions.codeWindow, }, initialDimension); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts index 3e3446349b8..0c3852191da 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts @@ -48,7 +48,7 @@ export class NotebookKernelHistoryService extends Disposable implements INoteboo // We will suggest the only kernel const suggested = allAvailableKernels.all.length === 1 ? allAvailableKernels.all[0] : undefined; this._notebookLoggingService.debug('History', `getMatchingKernels: ${allAvailableKernels.all.length} kernels available for ${notebook.uri.path}. Selected: ${allAvailableKernels.selected?.label}. Suggested: ${suggested?.label}`); - const mostRecentKernelIds = this._mostRecentKernelsMap[notebook.viewType] ? [...this._mostRecentKernelsMap[notebook.viewType].values()] : []; + const mostRecentKernelIds = this._mostRecentKernelsMap[notebook.notebookType] ? [...this._mostRecentKernelsMap[notebook.notebookType].values()] : []; const all = mostRecentKernelIds.map(kernelId => allKernels.find(kernel => kernel.id === kernelId)).filter(kernel => !!kernel) as INotebookKernel[]; this._notebookLoggingService.debug('History', `mru: ${mostRecentKernelIds.length} kernels in history, ${all.length} registered already.`); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts index 572a833e173..05174803267 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts @@ -37,12 +37,12 @@ class KernelInfo { class NotebookTextModelLikeId { static str(k: INotebookTextModelLike): string { - return `${k.viewType}/${k.uri.toString()}`; + return `${k.notebookType}/${k.uri.toString()}`; } static obj(s: string): INotebookTextModelLike { const idx = s.indexOf('/'); return { - viewType: s.substring(0, idx), + notebookType: s.substring(0, idx), uri: URI.parse(s.substring(idx + 1)) }; } @@ -178,7 +178,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel private static _score(kernel: INotebookKernel, notebook: INotebookTextModelLike): number { if (kernel.viewType === '*') { return 5; - } else if (kernel.viewType === notebook.viewType) { + } else if (kernel.viewType === notebook.notebookType) { return 10; } else { return 0; @@ -343,7 +343,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel const stateChangeListener = sourceAction.onDidChangeState(() => { this._onDidChangeSourceActions.fire({ notebook: document.uri, - viewType: document.viewType, + viewType: document.notebookType, }); }); sourceActions.push([sourceAction, stateChangeListener]); @@ -351,7 +351,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel }); info.actions = sourceActions; this._kernelSources.set(id, info); - this._onDidChangeSourceActions.fire({ notebook: document.uri, viewType: document.viewType }); + this._onDidChangeSourceActions.fire({ notebook: document.uri, viewType: document.notebookType }); }; this._kernelSourceActionsUpdates.get(id)?.dispose(); @@ -382,7 +382,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel } getKernelDetectionTasks(notebook: INotebookTextModelLike): INotebookKernelDetectionTask[] { - return this._kernelDetectionTasks.get(notebook.viewType) ?? []; + return this._kernelDetectionTasks.get(notebook.notebookType) ?? []; } registerKernelSourceActionProvider(viewType: string, provider: IKernelSourceActionProvider): IDisposable { @@ -411,7 +411,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel * Get kernel source actions from providers */ getKernelSourceActions2(notebook: INotebookTextModelLike): Promise { - const viewType = notebook.viewType; + const viewType = notebook.notebookType; const providers = this._kernelSourceActionProviders.get(viewType) ?? []; const promises = providers.map(provider => provider.provideKernelSourceActions()); return Promise.all(promises).then(actions => { diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index d512f580f14..86a0b01c560 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -13,7 +13,7 @@ import { Lazy } from 'vs/base/common/lazy'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; -import { isEqual } from 'vs/base/common/resources'; +import { basename, isEqual } from 'vs/base/common/resources'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -34,12 +34,14 @@ import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebo import { NotebookOutputRendererInfo, NotebookStaticPreloadInfo as NotebookStaticPreloadInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { DiffEditorInputFactoryFunction, EditorInputFactoryFunction, EditorInputFactoryObject, IEditorResolverService, IEditorType, RegisteredEditorInfo, RegisteredEditorPriority, UntitledEditorInputFactoryFunction } from 'vs/workbench/services/editor/common/editorResolverService'; +import { DiffEditorInputFactoryFunction, EditorInputFactoryFunction, EditorInputFactoryObject, IEditorResolverService, IEditorType, RegisteredEditorInfo, RegisteredEditorPriority, UntitledEditorInputFactoryFunction, type MergeEditorInputFactoryFunction } from 'vs/workbench/services/editor/common/editorResolverService'; import { IExtensionService, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { InstallRecommendedExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { INotebookDocument, INotebookDocumentService } from 'vs/workbench/services/notebook/common/notebookDocumentService'; +import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import type { EditorInputWithOptions, IResourceMergeEditorInput } from 'vs/workbench/common/editor'; export class NotebookProviderInfoStore extends Disposable { @@ -130,7 +132,6 @@ export class NotebookProviderInfoStore extends Disposable { selectors: notebookContribution.selector || [], priority: this._convertPriority(notebookContribution.priority), providerDisplayName: extension.description.displayName ?? extension.description.identifier.value, - exclusive: false })); } } @@ -169,7 +170,7 @@ export class NotebookProviderInfoStore extends Disposable { id: notebookProviderInfo.id, label: notebookProviderInfo.displayName, detail: notebookProviderInfo.providerDisplayName, - priority: notebookProviderInfo.exclusive ? RegisteredEditorPriority.exclusive : notebookProviderInfo.priority, + priority: notebookProviderInfo.priority, }; const notebookEditorOptions = { canHandleDiff: () => !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized(), @@ -195,7 +196,7 @@ export class NotebookProviderInfoStore extends Disposable { cellOptions = (options as INotebookEditorOptions | undefined)?.cellOptions; } - const notebookOptions = { ...options, cellOptions } as INotebookEditorOptions; + const notebookOptions: INotebookEditorOptions = { ...options, cellOptions, viewState: undefined }; const editor = NotebookEditorInput.getOrCreate(this._instantiationService, notebookUri, preferredResource, notebookProviderInfo.id); return { editor, options: notebookOptions }; }; @@ -214,11 +215,33 @@ export class NotebookProviderInfoStore extends Disposable { const notebookDiffEditorInputFactory: DiffEditorInputFactoryFunction = ({ modified, original, label, description }) => { return { editor: NotebookDiffEditorInput.create(this._instantiationService, modified.resource!, label, description, original.resource!, notebookProviderInfo.id) }; }; + const mergeEditorInputFactory: MergeEditorInputFactoryFunction = (mergeEditor: IResourceMergeEditorInput): EditorInputWithOptions => { + return { + editor: this._instantiationService.createInstance( + MergeEditorInput, + mergeEditor.base.resource, + { + uri: mergeEditor.input1.resource, + title: mergeEditor.input1.label ?? basename(mergeEditor.input1.resource), + description: mergeEditor.input1.description ?? '', + detail: mergeEditor.input1.detail + }, + { + uri: mergeEditor.input2.resource, + title: mergeEditor.input2.label ?? basename(mergeEditor.input2.resource), + description: mergeEditor.input2.description ?? '', + detail: mergeEditor.input2.detail + }, + mergeEditor.result.resource + ) + }; + }; const notebookFactoryObject: EditorInputFactoryObject = { createEditorInput: notebookEditorInputFactory, createDiffEditorInput: notebookDiffEditorInputFactory, createUntitledEditorInput: notebookUntitledEditorFactory, + createMergeEditorInput: mergeEditorInputFactory }; const notebookCellFactoryObject: EditorInputFactoryObject = { createEditorInput: notebookEditorInputFactory, @@ -615,8 +638,7 @@ export class NotebookService extends Disposable implements INotebookService { id: viewType, displayName: data.displayName, providerDisplayName: data.providerDisplayName, - exclusive: data.exclusive, - priority: RegisteredEditorPriority.default, + priority: data.priority || RegisteredEditorPriority.default, selectors: [] }); @@ -673,6 +695,14 @@ export class NotebookService extends Disposable implements INotebookService { return result; } + tryGetDataProviderSync(viewType: string): SimpleNotebookProviderInfo | undefined { + const selected = this.notebookProviderInfoStore.get(viewType); + if (!selected) { + return undefined; + } + return this._notebookProviders.get(selected.id); + } + private _persistMementos(): void { this._memento.saveMemento(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellPart.ts index 546637fae83..1ae8bf98ef5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellPart.ts @@ -16,7 +16,7 @@ import { ICellExecutionStateChangedEvent } from 'vs/workbench/contrib/notebook/c */ export abstract class CellContentPart extends Disposable { protected currentCell: ICellViewModel | undefined; - protected readonly cellDisposables = new DisposableStore(); + protected readonly cellDisposables = this._register(new DisposableStore()); constructor() { super(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts index 854358af69d..bfb757be79c 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts @@ -17,7 +17,7 @@ import { MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/ac import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; export class CodiconActionViewItem extends MenuEntryActionViewItem { @@ -49,7 +49,7 @@ export class ActionViewWithLabel extends MenuEntryActionViewItem { } export class UnifiedSubmenuActionView extends SubmenuEntryActionViewItem { private _actionLabel?: HTMLAnchorElement; - private _hover?: IUpdatableHover; + private _hover?: IManagedHover; private _primaryAction: IAction | undefined; constructor( @@ -73,7 +73,7 @@ export class UnifiedSubmenuActionView extends SubmenuEntryActionViewItem { this._actionLabel = document.createElement('a'); container.appendChild(this._actionLabel); - this._hover = this._register(this._hoverService.setupUpdatableHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('element'), this._actionLabel, '')); + this._hover = this._register(this._hoverService.setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('element'), this._actionLabel, '')); this.updateLabel(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts index b8ccbf07abf..dcdd1a08860 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce } from 'vs/base/common/arrays'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import * as languages from 'vs/editor/common/languages'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -15,20 +15,16 @@ import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentSe import { CommentThreadWidget } from 'vs/workbench/contrib/comments/browser/commentThreadWidget'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; export class CellComments extends CellContentPart { - private _initialized: boolean = false; - private _commentThreadWidget: CommentThreadWidget | null = null; - private currentElement: CodeCellViewModel | undefined; - private readonly commentTheadDisposables = this._register(new DisposableStore()); + private readonly _commentThreadWidget: MutableDisposable>; + private currentElement: ICellViewModel | undefined; + private readonly _commentThreadDisposables = this._register(new DisposableStore()); constructor( private readonly notebookEditor: INotebookEditorDelegate, private readonly container: HTMLElement, - @IContextKeyService private readonly contextKeyService: IContextKeyService, @IThemeService private readonly themeService: IThemeService, @ICommentService private readonly commentService: ICommentService, @@ -38,6 +34,8 @@ export class CellComments extends CellContentPart { super(); this.container.classList.add('review-widget'); + this._register(this._commentThreadWidget = new MutableDisposable>()); + this._register(this.themeService.onDidColorThemeChange(this._applyTheme, this)); // TODO @rebornix onDidChangeLayout (font change) // this._register(this.notebookEditor.onDidchangeLa) @@ -45,22 +43,17 @@ export class CellComments extends CellContentPart { } private async initialize(element: ICellViewModel) { - if (this._initialized) { + if (this.currentElement === element) { return; } - this._initialized = true; - const info = await this._getCommentThreadForCell(element); - - if (info) { - await this._createCommentTheadWidget(info.owner, info.thread); - } + this.currentElement = element; + await this._updateThread(); } private async _createCommentTheadWidget(owner: string, commentThread: languages.CommentThread) { - this._commentThreadWidget?.dispose(); - this.commentTheadDisposables.clear(); - this._commentThreadWidget = this.instantiationService.createInstance( + this._commentThreadDisposables.clear(); + this._commentThreadWidget.value = this.instantiationService.createInstance( CommentThreadWidget, this.container, this.notebookEditor, @@ -84,44 +77,47 @@ export class CellComments extends CellContentPart { const layoutInfo = this.notebookEditor.getLayoutInfo(); - await this._commentThreadWidget.display(layoutInfo.fontInfo.lineHeight, true); + await this._commentThreadWidget.value.display(layoutInfo.fontInfo.lineHeight, true); this._applyTheme(); - this.commentTheadDisposables.add(this._commentThreadWidget.onDidResize(() => { - if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget) { - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.getDimensions().height); + this._commentThreadDisposables.add(this._commentThreadWidget.value.onDidResize(() => { + if (this.currentElement && this._commentThreadWidget.value) { + this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value.getDimensions().height); } })); } private _bindListeners() { - this.cellDisposables.add(this.commentService.onDidUpdateCommentThreads(async () => { - if (this.currentElement) { - const info = await this._getCommentThreadForCell(this.currentElement); - if (!this._commentThreadWidget && info) { - await this._createCommentTheadWidget(info.owner, info.thread); - const layoutInfo = (this.currentElement as CodeCellViewModel).layoutInfo; - this.container.style.top = `${layoutInfo.outputContainerOffset + layoutInfo.outputTotalHeight}px`; - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget!.getDimensions().height); - return; - } + this.cellDisposables.add(this.commentService.onDidUpdateCommentThreads(async () => this._updateThread())); + } - if (this._commentThreadWidget) { - if (!info) { - this._commentThreadWidget.dispose(); - this.currentElement.commentHeight = 0; - return; - } - if (this._commentThreadWidget.commentThread === info.thread) { - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.getDimensions().height); - return; - } + private async _updateThread() { + if (!this.currentElement) { + return; + } + const info = await this._getCommentThreadForCell(this.currentElement); + if (!this._commentThreadWidget.value && info) { + await this._createCommentTheadWidget(info.owner, info.thread); + this.container.style.top = `${this.currentElement.layoutInfo.commentOffset}px`; + this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value!.getDimensions().height); + return; + } - await this._commentThreadWidget.updateCommentThread(info.thread); - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.getDimensions().height); - } + if (this._commentThreadWidget.value) { + if (!info) { + this._commentThreadDisposables.clear(); + this._commentThreadWidget.value = undefined; + this.currentElement.commentHeight = 0; + return; } - })); + if (this._commentThreadWidget.value.commentThread === info.thread) { + this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value.getDimensions().height); + return; + } + + await this._commentThreadWidget.value.updateCommentThread(info.thread); + this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value.getDimensions().height); + } } private _calculateCommentThreadHeight(bodyHeight: number) { @@ -151,28 +147,23 @@ export class CellComments extends CellContentPart { private _applyTheme() { const theme = this.themeService.getColorTheme(); const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; - this._commentThreadWidget?.applyTheme(theme, fontInfo); + this._commentThreadWidget.value?.applyTheme(theme, fontInfo); } override didRenderCell(element: ICellViewModel): void { - if (element.cellKind === CellKind.Code) { - this.currentElement = element as CodeCellViewModel; - this.initialize(element); - this._bindListeners(); - } - + this.initialize(element); + this._bindListeners(); } override prepareLayout(): void { - if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget) { - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.getDimensions().height); + if (this.currentElement && this._commentThreadWidget.value) { + this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.value.getDimensions().height); } } override updateInternalLayoutNow(element: ICellViewModel): void { - if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget) { - const layoutInfo = (element as CodeCellViewModel).layoutInfo; - this.container.style.top = `${layoutInfo.outputContainerOffset + layoutInfo.outputTotalHeight}px`; + if (this.currentElement && this._commentThreadWidget.value) { + this.container.style.top = `${element.layoutInfo.commentOffset}px`; } } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts index 06444079e65..407fc5eaa81 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts @@ -335,7 +335,7 @@ export class CellDragAndDropController extends Disposable { const dragImage = dragImageProvider(); cellRoot.parentElement!.appendChild(dragImage); event.dataTransfer.setDragImage(dragImage, 0, 0); - setTimeout(() => cellRoot.parentElement!.removeChild(dragImage), 0); // Comment this out to debug drag image layout + setTimeout(() => dragImage.remove(), 0); // Comment this out to debug drag image layout }; for (const dragHandle of dragHandles) { templateData.templateDisposables.add(DOM.addDisposableListener(dragHandle, DOM.EventType.DRAG_START, onDragStart)); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts index 7549fdeaab8..189ef50f166 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts @@ -77,7 +77,7 @@ export class CellFocusIndicator extends CellContentPart { override updateInternalLayoutNow(element: ICellViewModel): void { if (element.cellKind === CellKind.Markup) { const indicatorPostion = this.notebookEditor.notebookOptions.computeIndicatorPosition(element.layoutInfo.totalHeight, (element as MarkupCellViewModel).layoutInfo.foldHintHeight, this.notebookEditor.textModel?.viewType); - this.bottom.domNode.style.transform = `translateY(${indicatorPostion.bottomIndicatorTop}px)`; + this.bottom.domNode.style.transform = `translateY(${indicatorPostion.bottomIndicatorTop + 6}px)`; this.left.setHeight(indicatorPostion.verticalIndicatorHeight); this.right.setHeight(indicatorPostion.verticalIndicatorHeight); this.codeFocusIndicator.setHeight(indicatorPostion.verticalIndicatorHeight - this.getIndicatorTopMargin() * 2 - element.layoutInfo.chatHeight); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index 6cb5b68da50..5300567754b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -33,8 +33,9 @@ import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKe import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { COPY_OUTPUT_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/controller/cellOutputActions'; -import { CLEAR_CELL_OUTPUTS_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/controller/editActions'; import { TEXT_BASED_MIMETYPES } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard'; +import { autorun, observableValue } from 'vs/base/common/observable'; +import { NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS, NOTEBOOK_CELL_IS_FIRST_OUTPUT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; interface IMimeTypeRenderer extends IQuickPickItem { index: number; @@ -60,13 +61,14 @@ interface IRenderResult { // | | #cell-output-toolbar // | | #output-element class CellOutputElement extends Disposable { - private readonly _renderDisposableStore = this._register(new DisposableStore()); + private readonly toolbarDisposables = this._register(new DisposableStore()); innerContainer?: HTMLElement; renderedOutputContainer!: HTMLElement; renderResult?: IInsetRenderOutput; private readonly contextKeyService: IContextKeyService; + private toolbarAttached = false; constructor( private notebookEditor: INotebookEditorDelegate, @@ -95,7 +97,7 @@ class CellOutputElement extends Disposable { } detach() { - this.renderedOutputContainer?.parentElement?.removeChild(this.renderedOutputContainer); + this.renderedOutputContainer?.remove(); let count = 0; if (this.innerContainer) { @@ -110,7 +112,7 @@ class CellOutputElement extends Disposable { } if (count === 0) { - this.innerContainer.parentElement?.removeChild(this.innerContainer); + this.innerContainer.remove(); } } @@ -151,10 +153,10 @@ class CellOutputElement extends Disposable { } else { // Another mimetype or renderer is picked, we need to clear the current output and re-render const nextElement = this.innerContainer.nextElementSibling; - this._renderDisposableStore.clear(); + this.toolbarDisposables.clear(); const element = this.innerContainer; if (element) { - element.parentElement?.removeChild(element); + element.remove(); this.notebookEditor.removeInset(this.output); } @@ -207,7 +209,20 @@ class CellOutputElement extends Disposable { } const innerContainer = this._generateInnerOutputContainer(previousSibling, selectedPresentation); - this._attachToolbar(innerContainer, notebookTextModel, this.notebookEditor.activeKernel, index, mimeTypes); + if (index === 0 || this.output.visible.get()) { + this._attachToolbar(innerContainer, notebookTextModel, this.notebookEditor.activeKernel, index, mimeTypes); + } else { + this._register(autorun((reader) => { + const visible = reader.readObservable(this.output.visible); + if (visible && !this.toolbarAttached) { + this._attachToolbar(innerContainer, notebookTextModel, this.notebookEditor.activeKernel, index, mimeTypes); + } else if (!visible) { + this.toolbarDisposables.clear(); + } + this.cellOutputContainer.checkForHiddenOutputs(); + })); + this.cellOutputContainer.hasHiddenOutputs.set(true, undefined); + } this.renderedOutputContainer = DOM.append(innerContainer, DOM.$('.rendered-output')); @@ -291,14 +306,12 @@ class CellOutputElement extends Disposable { return; } - const useConsolidatedButton = this.notebookEditor.notebookOptions.getDisplayOptions().consolidatedOutputButton; - outputItemDiv.style.position = 'relative'; const mimeTypePicker = DOM.$('.cell-output-toolbar'); outputItemDiv.appendChild(mimeTypePicker); - const toolbar = this._renderDisposableStore.add(this.instantiationService.createInstance(WorkbenchToolBar, mimeTypePicker, { + const toolbar = this.toolbarDisposables.add(this.instantiationService.createInstance(WorkbenchToolBar, mimeTypePicker, { renderDropdownAsChildElement: false })); toolbar.context = { @@ -310,20 +323,22 @@ class CellOutputElement extends Disposable { }; // TODO: This could probably be a real registered action, but it has to talk to this output element - const pickAction = new Action('notebook.output.pickMimetype', nls.localize('pickMimeType', "Change Presentation"), ThemeIcon.asClassName(mimetypeIcon), undefined, - async _context => this._pickActiveMimeTypeRenderer(outputItemDiv, notebookTextModel, kernel, this.output)); + const pickAction = this.toolbarDisposables.add(new Action('notebook.output.pickMimetype', nls.localize('pickMimeType', "Change Presentation"), ThemeIcon.asClassName(mimetypeIcon), undefined, + async _context => this._pickActiveMimeTypeRenderer(outputItemDiv, notebookTextModel, kernel, this.output))); + + const menuContextKeyService = this.toolbarDisposables.add(this.contextKeyService.createScoped(outputItemDiv)); + const hasHiddenOutputs = NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS.bindTo(menuContextKeyService); + const isFirstCellOutput = NOTEBOOK_CELL_IS_FIRST_OUTPUT.bindTo(menuContextKeyService); + isFirstCellOutput.set(index === 0); + this.toolbarDisposables.add(autorun((reader) => { hasHiddenOutputs.set(reader.readObservable(this.cellOutputContainer.hasHiddenOutputs)); })); + const menu = this.toolbarDisposables.add(this.menuService.createMenu(MenuId.NotebookOutputToolbar, menuContextKeyService)); - const menu = this._renderDisposableStore.add(this.menuService.createMenu(MenuId.NotebookOutputToolbar, this.contextKeyService)); const updateMenuToolbar = () => { const primary: IAction[] = []; let secondary: IAction[] = []; const result = { primary, secondary }; - createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result, () => false); - if (index > 0 || !useConsolidatedButton) { - // clear outputs should only appear in the first output item's menu - secondary = secondary.filter((action) => action.id !== CLEAR_CELL_OUTPUTS_COMMAND_ID); - } + createAndFillInActionBarActions(menu!, { shouldForwardArgs: true }, result, () => false); if (!isCopyEnabled) { secondary = secondary.filter((action) => action.id !== COPY_OUTPUT_COMMAND_ID); } @@ -334,8 +349,7 @@ class CellOutputElement extends Disposable { toolbar.setActions([], secondary); }; updateMenuToolbar(); - this._renderDisposableStore.add(menu.onDidChange(updateMenuToolbar)); - + this.toolbarDisposables.add(menu.onDidChange(updateMenuToolbar)); } private async _pickActiveMimeTypeRenderer(outputItemDiv: HTMLElement, notebookTextModel: NotebookTextModel, kernel: INotebookKernel | undefined, viewModel: ICellOutputViewModel) { @@ -397,10 +411,10 @@ class CellOutputElement extends Disposable { // user chooses another mimetype const nextElement = outputItemDiv.nextElementSibling; - this._renderDisposableStore.clear(); + this.toolbarDisposables.clear(); const element = this.innerContainer; if (element) { - element.parentElement?.removeChild(element); + element.remove(); this.notebookEditor.removeInset(viewModel); } @@ -479,6 +493,15 @@ export class CellOutputContainer extends CellContentPart { private _outputEntries: OutputEntryViewHandler[] = []; private _hasStaleOutputs: boolean = false; + hasHiddenOutputs = observableValue('hasHiddenOutputs', false); + checkForHiddenOutputs() { + if (this._outputEntries.find(entry => { return entry.model.visible; })) { + this.hasHiddenOutputs.set(true, undefined); + } else { + this.hasHiddenOutputs.set(false, undefined); + } + } + get renderedOutputEntries() { return this._outputEntries; } @@ -807,5 +830,3 @@ const JUPYTER_RENDERER_MIMETYPES = [ 'application/vnd.jupyter.widget-view+json', 'application/vnd.code.notebook.error' ]; - - diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts index 98b202b5b1c..a4514e41d4d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts @@ -31,7 +31,7 @@ import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/ import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; -import type { IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; const $ = DOM.$; @@ -123,12 +123,15 @@ export class CellEditorStatusBar extends CellContentPart { override didRenderCell(element: ICellViewModel): void { - this.updateContext({ - ui: true, - cell: element, - notebookEditor: this._notebookEditor, - $mid: MarshalledId.NotebookCellActionContext - }); + if (this._notebookEditor.hasModel()) { + const context: (INotebookCellActionContext & { $mid: number }) = { + ui: true, + cell: element, + notebookEditor: this._notebookEditor, + $mid: MarshalledId.NotebookCellActionContext + }; + this.updateContext(context); + } if (this._editor) { // Focus Mode @@ -235,7 +238,7 @@ export class CellEditorStatusBar extends CellContentPart { if (renderedItems.length > newItems.length) { const deleted = renderedItems.splice(newItems.length, renderedItems.length - newItems.length); for (const deletedItem of deleted) { - container.removeChild(deletedItem.container); + deletedItem.container.remove(); deletedItem.dispose(); } } @@ -327,8 +330,8 @@ class CellStatusBarItem extends Disposable { this.container.setAttribute('role', role || ''); if (item.tooltip) { - const hoverContent = typeof item.tooltip === 'string' ? item.tooltip : { markdown: item.tooltip } as IUpdatableHoverTooltipMarkdownString; - this._itemDisposables.add(this._hoverService.setupUpdatableHover(this._hoverDelegate, this.container, hoverContent)); + const hoverContent = typeof item.tooltip === 'string' ? item.tooltip : { markdown: item.tooltip, markdownNotSupportedFallback: undefined } satisfies IManagedHoverTooltipMarkdownString; + this._itemDisposables.add(this._hoverService.setupManagedHover(this._hoverDelegate, this.container, hoverContent)); } this.container.classList.toggle('cell-status-item-has-command', !!item.command); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts index 733ac19d6bb..36911bfbdbb 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts @@ -80,13 +80,15 @@ export class BetweenCellToolbar extends CellOverlayPart { override didRenderCell(element: ICellViewModel): void { const betweenCellToolbar = this._initialize(); - betweenCellToolbar.context = { - ui: true, - cell: element, - notebookEditor: this._notebookEditor, - source: 'insertToolbar', - $mid: MarshalledId.NotebookCellActionContext - }; + if (this._notebookEditor.hasModel()) { + betweenCellToolbar.context = { + ui: true, + cell: element, + notebookEditor: this._notebookEditor, + source: 'insertToolbar', + $mid: MarshalledId.NotebookCellActionContext + } satisfies (INotebookCellActionContext & { source?: string; $mid: number }); + } this.updateInternalLayoutNow(element); } @@ -202,13 +204,17 @@ export class CellTitleToolbarPart extends CellOverlayPart { const view = this._initialize(model, element); this.cellDisposables.add(registerCellToolbarStickyScroll(this._notebookEditor, element, this.toolbarContainer, { extraOffset: 4, min: -14 })); - this.updateContext(view, { - ui: true, - cell: element, - notebookEditor: this._notebookEditor, - source: 'cellToolbar', - $mid: MarshalledId.NotebookCellActionContext - }); + if (this._notebookEditor.hasModel()) { + const toolbarContext: INotebookCellActionContext & { source?: string; $mid: number } = { + ui: true, + cell: element, + notebookEditor: this._notebookEditor, + source: 'cellToolbar', + $mid: MarshalledId.NotebookCellActionContext + }; + + this.updateContext(view, toolbarContext); + } } private updateContext(view: CellTitleToolbarView, toolbarContext: INotebookCellActionContext) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index 45ccce37658..4370f036558 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -128,7 +128,7 @@ export class CodeCell extends Disposable { const executionItemElement = DOM.append(this.templateData.cellInputCollapsedContainer, DOM.$('.collapsed-execution-icon')); this._register(toDisposable(() => { - executionItemElement.parentElement?.removeChild(executionItemElement); + executionItemElement.remove(); })); this._collapsedExecutionIcon = this._register(this.instantiationService.createInstance(CollapsedCodeCellExecutionIcon, this.notebookEditor, this.viewCell, executionItemElement)); this.updateForCollapseState(); @@ -496,7 +496,7 @@ export class CodeCell extends Disposable { } elements.forEach(element => { - element.parentElement?.removeChild(element); + element.remove(); }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts index de2c0e912bf..c6f345362ea 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts @@ -58,12 +58,15 @@ export class RunToolbar extends CellContentPart { override didRenderCell(element: ICellViewModel): void { this.cellDisposables.add(registerCellToolbarStickyScroll(this.notebookEditor, element, this.runButtonContainer)); - this.toolbar.context = { - ui: true, - cell: element, - notebookEditor: this.notebookEditor, - $mid: MarshalledId.NotebookCellActionContext - }; + if (this.notebookEditor.hasModel()) { + const context: INotebookCellActionContext & { $mid: number } = { + ui: true, + cell: element, + notebookEditor: this.notebookEditor, + $mid: MarshalledId.NotebookCellActionContext + }; + this.toolbar.context = context; + } } getCellToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IAction[] } { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts index d334bb1db3f..7fbe705a0a0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts @@ -228,12 +228,14 @@ export class MarkupCell extends Disposable { e.added.forEach(options => { if (options.className) { this.notebookEditor.deltaCellContainerClassNames(this.viewCell.id, [options.className], []); + this.templateData.rootContainer.classList.add(options.className); } }); e.removed.forEach(options => { if (options.className) { this.notebookEditor.deltaCellContainerClassNames(this.viewCell.id, [], [options.className]); + this.templateData.rootContainer.classList.remove(options.className); } }); })); @@ -241,6 +243,7 @@ export class MarkupCell extends Disposable { this.viewCell.getCellDecorations().forEach(options => { if (options.className) { this.notebookEditor.deltaCellContainerClassNames(this.viewCell.id, [options.className], []); + this.templateData.rootContainer.classList.add(options.className); } }); } @@ -346,7 +349,7 @@ export class MarkupCell extends Disposable { // create a special context key service that set the inCompositeEditor-contextkey const editorContextKeyService = this.contextKeyService.createScoped(this.templateData.editorPart); EditorContextKeys.inCompositeEditor.bindTo(editorContextKeyService).set(true); - const editorInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editorContextKeyService])); + const editorInstaService = this.editorDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editorContextKeyService]))); this.editorDisposables.add(editorContextKeyService); this.editor = this.editorDisposables.add(editorInstaService.createInstance(CodeEditorWidget, this.templateData.editorContainer, { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 702182063b3..51c11f43383 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -30,7 +30,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { ITextEditorOptions, ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -270,7 +270,7 @@ export class BackLayerWebView extends Themable { 'notebook-markdown-line-height': typeof this.options.markdownLineHeight === 'number' && this.options.markdownLineHeight > 0 ? `${this.options.markdownLineHeight}px` : `normal`, 'notebook-cell-output-font-size': `${this.options.outputFontSize || this.options.fontSize}px`, 'notebook-cell-output-line-height': `${this.options.outputLineHeight}px`, - 'notebook-cell-output-max-height': `${this.options.outputLineHeight * this.options.outputLineLimit}px`, + 'notebook-cell-output-max-height': `${this.options.outputLineHeight * this.options.outputLineLimit + 2}px`, 'notebook-cell-output-font-family': this.options.outputFontFamily || this.options.fontFamily, 'notebook-cell-markup-empty-content': nls.localize('notebook.emptyMarkdownPlaceholder', "Empty markdown cell, double-click or press enter to edit."), 'notebook-cell-renderer-not-found-error': nls.localize({ @@ -401,6 +401,14 @@ export class BackLayerWebView extends Themable { background-color: var(--theme-notebook-symbol-highlight-background); } + #container .markup > div.nb-multiCellHighlight { + background-color: var(--theme-notebook-symbol-highlight-background); + } + + #container .nb-multiCellHighlight .output_container .output { + background-color: var(--theme-notebook-symbol-highlight-background); + } + #container .nb-chatGenerationHighlight .output_container .output { background-color: var(--vscode-notebook-selectedCellBackground); } @@ -428,7 +436,7 @@ export class BackLayerWebView extends Themable { } table, thead, tr, th, td, tbody { - border: none !important; + border: none; border-color: transparent; border-spacing: 0; border-collapse: collapse; @@ -1111,7 +1119,9 @@ export class BackLayerWebView extends Themable { } if (match) { - match.group.openEditor(match.editor, lineNumber !== undefined && column !== undefined ? { selection: { startLineNumber: lineNumber, startColumn: column } } : undefined); + const selection: ITextEditorSelection | undefined = lineNumber !== undefined && column !== undefined ? { startLineNumber: lineNumber, startColumn: column } : undefined; + const textEditorOptions: ITextEditorOptions = { selection: selection }; + match.group.openEditor(match.editor, selection ? textEditorOptions : undefined); } else { this.openerService.open(uri, { fromUserGesture: true, fromWorkspace: true }); } @@ -1772,7 +1782,7 @@ export class BackLayerWebView extends Themable { }); } - async find(query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string }): Promise { + async find(query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string; findIds: string[] }): Promise { if (query === '') { this._sendMessageToWebview({ type: 'findStop', 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 9f6f5b8dac5..564ddd06414 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -157,7 +157,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen const innerContent = DOM.append(container, $('.cell.markdown')); const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); - const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); + const scopedInstaService = templateDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]))); const rootClassDelegate = { toggle: (className: string, force?: boolean) => container.classList.toggle(className, force) }; @@ -279,7 +279,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende // create a special context key service that set the inCompositeEditor-contextkey const editorContextKeyService = templateDisposables.add(this.contextKeyServiceProvider(editorPart)); - const editorInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editorContextKeyService])); + const editorInstaService = templateDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editorContextKeyService]))); EditorContextKeys.inCompositeEditor.bindTo(editorContextKeyService).set(true); const editor = editorInstaService.createInstance(CodeEditorWidget, editorContainer, { @@ -303,7 +303,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const bottomCellToolbarContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); const focusIndicatorBottom = new FastDomNode(DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-bottom'))); - const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); + const scopedInstaService = templateDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]))); const rootClassDelegate = { toggle: (className: string, force?: boolean) => container.classList.toggle(className, force) }; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index 5d0e13ad3b2..a59abdada13 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -399,7 +399,7 @@ export interface ITokenizedStylesChangedMessage { export interface IFindMessage { readonly type: 'find'; readonly query: string; - readonly options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string }; + readonly options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string; findIds: string[] }; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 6e4aed1f573..f80e4f40ccc 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -90,7 +90,7 @@ declare function __import(path: string): Promise; async function webviewPreloads(ctx: PreloadContext) { - /* eslint-disable no-restricted-globals */ + /* eslint-disable no-restricted-globals, no-restricted-syntax */ // The use of global `window` should be fine in this context, even // with aux windows. This code is running from within an `iframe` @@ -461,7 +461,7 @@ async function webviewPreloads(ctx: PreloadContext) { id, height, init: update.init, - isOutput: update.isOutput, + isOutput: update.isOutput }); } else { this.pending.set(id, { @@ -484,6 +484,11 @@ async function webviewPreloads(ctx: PreloadContext) { } }; + function elementHasContent(height: number) { + // we need to account for a potential 1px top and bottom border on a child within the output container + return height > 2.1; + } + const resizeObserver = new class { private readonly _observer: ResizeObserver; @@ -519,23 +524,23 @@ async function webviewPreloads(ctx: PreloadContext) { continue; } - const newHeight = entry.contentRect.height; + const hasContent = elementHasContent(entry.contentRect.height); const shouldUpdatePadding = - (newHeight !== 0 && observedElementInfo.lastKnownPadding === 0) || - (newHeight === 0 && observedElementInfo.lastKnownPadding !== 0); + (hasContent && observedElementInfo.lastKnownPadding === 0) || + (!hasContent && observedElementInfo.lastKnownPadding !== 0); if (shouldUpdatePadding) { // Do not update dimension in resize observer window.requestAnimationFrame(() => { - if (newHeight !== 0) { + if (hasContent) { entry.target.style.padding = `${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodeLeftPadding}px`; } else { entry.target.style.padding = `0px`; } - this.updateHeight(observedElementInfo, entry.target.offsetHeight); + this.updateHeight(observedElementInfo, hasContent ? entry.target.offsetHeight : 0); }); } else { - this.updateHeight(observedElementInfo, entry.target.offsetHeight); + this.updateHeight(observedElementInfo, hasContent ? entry.target.offsetHeight : 0); } } }); @@ -1425,9 +1430,9 @@ async function webviewPreloads(ctx: PreloadContext) { return offset + getSelectionOffsetRelativeTo(parentElement, currentNode.parentNode); } - const find = (query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string }) => { + const find = (query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string; findIds: string[] }) => { let find = true; - const matches: IFindMatch[] = []; + let matches: IFindMatch[] = []; const range = document.createRange(); range.selectNodeContents(window.document.getElementById('findStart')!); @@ -1553,6 +1558,8 @@ async function webviewPreloads(ctx: PreloadContext) { console.log(e); } + + matches = matches.filter(match => options.findIds.length ? options.findIds.includes(match.cellId) : true); _highlighter.addHighlights(matches, options.ownerID); window.document.getSelection()?.removeAllRanges(); @@ -2753,10 +2760,6 @@ async function webviewPreloads(ctx: PreloadContext) { this.element.style.visibility = ''; this.element.style.top = `${top}px`; - - dimensionUpdater.updateHeight(outputId, outputContainer.element.offsetHeight, { - isOutput: true, - }); } public hide() { @@ -2939,17 +2942,26 @@ async function webviewPreloads(ctx: PreloadContext) { const offsetHeight = this.element.offsetHeight; const cps = document.defaultView!.getComputedStyle(this.element); - if (offsetHeight !== 0 && cps.padding === '0px') { - // we set padding to zero if the output height is zero (then we can have a zero-height output DOM node) + const verticalPadding = parseFloat(cps.paddingTop) + parseFloat(cps.paddingBottom); + const contentHeight = offsetHeight - verticalPadding; + if (elementHasContent(contentHeight) && cps.padding === '0px') { + // we set padding to zero if the output has no content (then we can have a zero-height output DOM node) // thus we need to ensure the padding is accounted when updating the init height of the output dimensionUpdater.updateHeight(this.outputId, offsetHeight + ctx.style.outputNodePadding * 2, { isOutput: true, - init: true, + init: true }); this.element.style.padding = `${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodeLeftPadding}`; - } else { + } else if (elementHasContent(contentHeight)) { dimensionUpdater.updateHeight(this.outputId, this.element.offsetHeight, { + isOutput: true, + init: true + }); + this.element.style.padding = `0 ${ctx.style.outputNodePadding}px 0 ${ctx.style.outputNodeLeftPadding}`; + } else { + // we have a zero-height output DOM node + dimensionUpdater.updateHeight(this.outputId, 0, { isOutput: true, init: true, }); @@ -3067,7 +3079,7 @@ async function webviewPreloads(ctx: PreloadContext) { }); if (this.dragOverlay) { - window.document.body.removeChild(this.dragOverlay); + this.dragOverlay.remove(); this.dragOverlay = undefined; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index f946c2142d9..9ed29128b2c 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -9,7 +9,7 @@ import { Mimes } from 'vs/base/common/mime'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IPosition } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; @@ -19,12 +19,12 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { IWordWrapTransientState, readTransientState, writeTransientState } from 'vs/workbench/contrib/codeEditor/browser/toggleWordWrap'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CellEditState, CellFocusMode, CursorAtBoundary, CursorAtLineBoundary, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, CellLayoutChangeEvent, CursorAtBoundary, CursorAtLineBoundary, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, INotebookCellStatusBarItem, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, INotebookCellStatusBarItem, INotebookFindOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export abstract class BaseCellViewModel extends Disposable { @@ -158,6 +158,16 @@ export abstract class BaseCellViewModel extends Disposable { this._onDidChangeState.fire({ outputCollapsedChanged: true }); } + protected _commentHeight = 0; + + set commentHeight(height: number) { + if (this._commentHeight === height) { + return; + } + this._commentHeight = height; + this.layoutChange({ commentHeight: true }, 'BaseCellViewModel#commentHeight'); + } + private _isDisposed = false; constructor( @@ -204,7 +214,7 @@ export abstract class BaseCellViewModel extends Disposable { abstract updateOptions(e: NotebookOptionsChangeEvent): void; abstract getHeight(lineHeight: number): number; abstract onDeselect(): void; - abstract layoutChange(change: any): void; + abstract layoutChange(change: CellLayoutChangeEvent, source?: string): void; assertTextModelAttached(): boolean { if (this.textModel && this._textEditor && this._textEditor.getModel() === this.textModel) { @@ -258,7 +268,12 @@ export abstract class BaseCellViewModel extends Disposable { writeTransientState(editor.getModel(), this._editorTransientState, this._codeEditorService); } - this._textEditor?.changeDecorations((accessor) => { + if (this._isDisposed) { + // Restore View State could adjust the editor layout and trigger a list view update. The list view update might then dispose this view model. + return; + } + + editor.changeDecorations((accessor) => { this._resolvedDecorations.forEach((value, key) => { if (key.startsWith('_lazy_')) { // lazy ones @@ -272,7 +287,7 @@ export abstract class BaseCellViewModel extends Disposable { }); }); - this._editorListeners.push(this._textEditor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); })); + this._editorListeners.push(editor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); })); const inlineChatController = InlineChatController.get(this._textEditor); if (inlineChatController) { this._editorListeners.push(inlineChatController.onWillStartSession(() => { @@ -281,7 +296,7 @@ export abstract class BaseCellViewModel extends Disposable { } })); } - // this._editorListeners.push(this._textEditor.onKeyDown(e => this.handleKeyDown(e))); + this._onDidChangeState.fire({ selectionChanged: true }); this._onDidChangeEditorAttachState.fire(); } @@ -645,20 +660,21 @@ export abstract class BaseCellViewModel extends Disposable { protected abstract onDidChangeTextModelContent(): void; - protected cellStartFind(value: string, options: INotebookSearchOptions): model.FindMatch[] | null { + protected cellStartFind(value: string, options: INotebookFindOptions): model.FindMatch[] | null { let cellMatches: model.FindMatch[] = []; + const lineCount = this.textBuffer.getLineCount(); + const findRange: IRange[] = options.findScope?.selectedTextRanges ?? [new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1)]; + if (this.assertTextModelAttached()) { cellMatches = this.textModel!.findMatches( value, - false, + findRange, options.regex || false, options.caseSensitive || false, options.wholeWord ? options.wordSeparators || null : null, options.regex || false); } else { - const lineCount = this.textBuffer.getLineCount(); - const fullRange = new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1); const searchParams = new SearchParams(value, options.regex || false, options.caseSensitive || false, options.wholeWord ? options.wordSeparators || null : null,); const searchData = searchParams.parseSearchRequest(); @@ -666,7 +682,9 @@ export abstract class BaseCellViewModel extends Disposable { return null; } - cellMatches = this.textBuffer.findMatchesLineByLine(fullRange, searchData, options.regex || false, 1000); + findRange.forEach(range => { + cellMatches.push(...this.textBuffer.findMatchesLineByLine(new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn), searchData, options.regex || false, 1000)); + }); } return cellMatches; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellOutputViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellOutputViewModel.ts index e8cf31df9e3..5c523a51fac 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/cellOutputViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellOutputViewModel.ts @@ -5,6 +5,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; +import { observableValue } from 'vs/base/common/observable'; import { ICellOutputViewModel, IGenericCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { ICellOutput, IOrderedMimeType, RENDERER_NOT_AVAILABLE } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -14,6 +15,22 @@ let handle = 0; export class CellOutputViewModel extends Disposable implements ICellOutputViewModel { private _onDidResetRendererEmitter = this._register(new Emitter()); readonly onDidResetRenderer = this._onDidResetRendererEmitter.event; + + private alwaysShow = false; + visible = observableValue('outputVisible', false); + setVisible(visible = true, force: boolean = false) { + if (!visible && this.alwaysShow) { + // we are forced to show, so no-op + return; + } + + if (force && visible) { + this.alwaysShow = true; + } + + this.visible.set(visible, undefined); + } + outputHandle = handle++; get model(): ICellOutput { return this._outputRawData; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 3f5a6c3e68d..3ef89aa05e3 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -5,25 +5,25 @@ import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; import { dispose } from 'vs/base/common/lifecycle'; +import { observableValue } from 'vs/base/common/observable'; import * as UUID from 'vs/base/common/uuid'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { PrefixSumComputer } from 'vs/editor/common/model/prefixSumComputer'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, CellLayoutState, ICellOutputViewModel, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatch, CellLayoutState, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, ICellOutputViewModel, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; +import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/cellOutputViewModel'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, INotebookSearchOptions, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; +import { CellKind, INotebookFindOptions, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellExecutionError, ICellExecutionStateChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { BaseCellViewModel } from './baseCellViewModel'; -import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { ICellExecutionError, ICellExecutionStateChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { observableValue } from 'vs/base/common/observable'; export const outputDisplayLimit = 500; @@ -80,16 +80,6 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod return this._chatHeight; } - private _commentHeight = 0; - - set commentHeight(height: number) { - if (this._commentHeight === height) { - return; - } - this._commentHeight = height; - this.layoutChange({ commentHeight: true }, 'CodeCellViewModel#commentHeight'); - } - private _hoveringOutput: boolean = false; public get outputIsHovered(): boolean { return this._hoveringOutput; @@ -193,6 +183,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod : 0, chatHeight: 0, statusBarHeight: 0, + commentOffset: 0, commentHeight: 0, outputContainerOffset: 0, outputTotalHeight: 0, @@ -289,11 +280,12 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod editorHeight, editorWidth, statusBarHeight, - commentHeight, outputContainerOffset, outputTotalHeight, outputShowMoreContainerHeight, outputShowMoreContainerOffset, + commentOffset: outputContainerOffset + outputTotalHeight, + commentHeight, totalHeight, codeIndicatorHeight, outputIndicatorHeight, @@ -330,11 +322,12 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod editorWidth, chatHeight: chatHeight, statusBarHeight: 0, - commentHeight, outputContainerOffset, outputTotalHeight, outputShowMoreContainerHeight, outputShowMoreContainerOffset, + commentOffset: outputContainerOffset + outputTotalHeight, + commentHeight, totalHeight, codeIndicatorHeight, outputIndicatorHeight, @@ -359,22 +352,9 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod super.restoreEditorViewState(editorViewStates); if (totalHeight !== undefined && this._layoutInfo.layoutState !== CellLayoutState.Measured) { this._layoutInfo = { - fontInfo: this._layoutInfo.fontInfo, - chatHeight: this._layoutInfo.chatHeight, - editorHeight: this._layoutInfo.editorHeight, - editorWidth: this._layoutInfo.editorWidth, - statusBarHeight: this.layoutInfo.statusBarHeight, - commentHeight: this.layoutInfo.commentHeight, - outputContainerOffset: this._layoutInfo.outputContainerOffset, - outputTotalHeight: this._layoutInfo.outputTotalHeight, - outputShowMoreContainerHeight: this._layoutInfo.outputShowMoreContainerHeight, - outputShowMoreContainerOffset: this._layoutInfo.outputShowMoreContainerOffset, + ...this._layoutInfo, totalHeight: totalHeight, - codeIndicatorHeight: this._layoutInfo.codeIndicatorHeight, - outputIndicatorHeight: this._layoutInfo.outputIndicatorHeight, - bottomToolbarOffset: this._layoutInfo.bottomToolbarOffset, layoutState: CellLayoutState.FromCache, - estimatedHasHorizontalScrolling: this._layoutInfo.estimatedHasHorizontalScrolling }; } } @@ -464,7 +444,14 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } this._ensureOutputsTop(); - if (height < 28 && this._outputViewModels[index].hasMultiMimeType()) { + + if (index === 0 || height > 0) { + this._outputViewModels[index].setVisible(true); + } else if (height === 0) { + this._outputViewModels[index].setVisible(false); + } + + if (this._outputViewModels[index].visible.get() && height < 28) { height = 28; } @@ -518,7 +505,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod private readonly _hasFindResult = this._register(new Emitter()); public readonly hasFindResult: Event = this._hasFindResult.event; - startFind(value: string, options: INotebookSearchOptions): CellFindMatch | null { + startFind(value: string, options: INotebookFindOptions): CellFindMatch | null { const matches = super.cellStartFind(value, options); if (matches === null) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts index 0f255cc20db..3652f955558 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts @@ -10,7 +10,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { CellEditState, CellFindMatch, CellFoldingState, CellLayoutContext, CellLayoutState, EditorFoldingStateDelegate, ICellOutputViewModel, ICellViewModel, MarkupCellLayoutChangeEvent, MarkupCellLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, INotebookFindOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; @@ -134,6 +134,8 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM editorWidth: initialNotebookLayoutInfo?.width ? this.viewContext.notebookOptions.computeMarkdownCellEditorWidth(initialNotebookLayoutInfo.width) : 0, + commentOffset: 0, + commentHeight: 0, bottomToolbarOffset: bottomToolbarGap, totalHeight: 100, layoutState: CellLayoutState.Uninitialized, @@ -160,13 +162,14 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM + layoutConfiguration.markdownCellTopMargin + layoutConfiguration.markdownCellBottomMargin + bottomToolbarGap - + this._statusBarHeight; + + this._statusBarHeight + + this._commentHeight; } else { // @rebornix // On file open, the previewHeight + bottomToolbarGap for a cell out of viewport can be 0 // When it's 0, the list view will never try to render it anymore even if we scroll the cell into view. // Thus we make sure it's greater than 0 - return Math.max(1, this._previewHeight + bottomToolbarGap + foldHintHeight); + return Math.max(1, this._previewHeight + bottomToolbarGap + foldHintHeight + this._commentHeight); } } @@ -204,50 +207,58 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM } layoutChange(state: MarkupCellLayoutChangeEvent) { - // recompute - const foldHintHeight = this._computeFoldHintHeight(); + let totalHeight: number; + let foldHintHeight: number; if (!this.isInputCollapsed) { - const editorWidth = state.outerWidth !== undefined - ? this.viewContext.notebookOptions.computeMarkdownCellEditorWidth(state.outerWidth) - : this._layoutInfo.editorWidth; - const totalHeight = state.totalHeight === undefined - ? (this._layoutInfo.layoutState === CellLayoutState.Uninitialized ? 100 : this._layoutInfo.totalHeight) - : state.totalHeight; - const previewHeight = this._previewHeight; - - this._layoutInfo = { - fontInfo: state.font || this._layoutInfo.fontInfo, - editorWidth, - previewHeight, - chatHeight: this._chatHeight, - editorHeight: this._editorHeight, - statusBarHeight: this._statusBarHeight, - bottomToolbarOffset: this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType), - totalHeight, - layoutState: CellLayoutState.Measured, - foldHintHeight - }; + totalHeight = state.totalHeight === undefined ? + (this._layoutInfo.layoutState === + CellLayoutState.Uninitialized ? + 100 : + this._layoutInfo.totalHeight) : + state.totalHeight; + // recompute + foldHintHeight = this._computeFoldHintHeight(); } else { - const editorWidth = state.outerWidth !== undefined - ? this.viewContext.notebookOptions.computeMarkdownCellEditorWidth(state.outerWidth) - : this._layoutInfo.editorWidth; - const totalHeight = this.viewContext.notebookOptions.computeCollapsedMarkdownCellHeight(this.viewType); - + totalHeight = + this.viewContext.notebookOptions + .computeCollapsedMarkdownCellHeight(this.viewType); state.totalHeight = totalHeight; - this._layoutInfo = { - fontInfo: state.font || this._layoutInfo.fontInfo, - editorWidth, - chatHeight: this._chatHeight, - editorHeight: this._editorHeight, - statusBarHeight: this._statusBarHeight, - previewHeight: this._previewHeight, - bottomToolbarOffset: this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight, this.viewType), - totalHeight, - layoutState: CellLayoutState.Measured, - foldHintHeight: 0 - }; + foldHintHeight = 0; } + let commentOffset: number; + if (this.getEditState() === CellEditState.Editing) { + const notebookLayoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration(); + commentOffset = notebookLayoutConfiguration.editorToolbarHeight + + notebookLayoutConfiguration.cellTopMargin // CELL_TOP_MARGIN + + this._chatHeight + + this._editorHeight + + this._statusBarHeight; + } else { + commentOffset = this._previewHeight; + } + + this._layoutInfo = { + fontInfo: state.font || this._layoutInfo.fontInfo, + editorWidth: state.outerWidth !== undefined ? + this.viewContext.notebookOptions + .computeMarkdownCellEditorWidth(state.outerWidth) : + this._layoutInfo.editorWidth, + chatHeight: this._chatHeight, + editorHeight: this._editorHeight, + statusBarHeight: this._statusBarHeight, + previewHeight: this._previewHeight, + bottomToolbarOffset: this.viewContext.notebookOptions + .computeBottomToolbarOffset( + totalHeight, this.viewType), + totalHeight, + layoutState: CellLayoutState.Measured, + foldHintHeight, + commentOffset, + commentHeight: state.commentHeight ? + this._commentHeight : + this._layoutInfo.commentHeight, + }; this._onDidChangeLayout.fire(state); } @@ -257,16 +268,12 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM // we might already warmup the viewport so the cell has a total height computed if (totalHeight !== undefined && this.layoutInfo.layoutState === CellLayoutState.Uninitialized) { this._layoutInfo = { - fontInfo: this._layoutInfo.fontInfo, - editorWidth: this._layoutInfo.editorWidth, - previewHeight: this._layoutInfo.previewHeight, - bottomToolbarOffset: this._layoutInfo.bottomToolbarOffset, + ...this.layoutInfo, totalHeight: totalHeight, chatHeight: this._chatHeight, editorHeight: this._editorHeight, statusBarHeight: this._statusBarHeight, layoutState: CellLayoutState.FromCache, - foldHintHeight: this._layoutInfo.foldHintHeight }; this.layoutChange({}); } @@ -295,7 +302,7 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM private readonly _hasFindResult = this._register(new Emitter()); public readonly hasFindResult: Event = this._hasFindResult.event; - startFind(value: string, options: INotebookSearchOptions): CellFindMatch | null { + startFind(value: string, options: INotebookFindOptions): CellFindMatch | null { const matches = super.cellStartFind(value, options); if (matches === null) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts new file mode 100644 index 00000000000..767b9f8f8de --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts @@ -0,0 +1,222 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { IActiveNotebookEditor, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { OutlineChangeEvent, OutlineConfigKeys } from 'vs/workbench/services/outline/browser/outline'; +import { OutlineEntry } from './OutlineEntry'; +import { IOutlineModelService } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; + +export interface INotebookCellOutlineDataSource { + readonly activeElement: OutlineEntry | undefined; + readonly entries: OutlineEntry[]; +} + +export class NotebookCellOutlineDataSource implements INotebookCellOutlineDataSource { + + private readonly _disposables = new DisposableStore(); + + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + private _uri: URI | undefined; + private _entries: OutlineEntry[] = []; + private _activeEntry?: OutlineEntry; + + private readonly _outlineEntryFactory: NotebookOutlineEntryFactory; + + constructor( + private readonly _editor: INotebookEditor, + @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, + @IOutlineModelService private readonly _outlineModelService: IOutlineModelService, + @IMarkerService private readonly _markerService: IMarkerService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + this._outlineEntryFactory = new NotebookOutlineEntryFactory(this._notebookExecutionStateService); + this.recomputeState(); + } + + get activeElement(): OutlineEntry | undefined { + return this._activeEntry; + } + get entries(): OutlineEntry[] { + return this._entries; + } + get isEmpty(): boolean { + return this._entries.length === 0; + } + get uri() { + return this._uri; + } + + public async computeFullSymbols(cancelToken: CancellationToken) { + const notebookEditorWidget = this._editor; + + const notebookCells = notebookEditorWidget?.getViewModel()?.viewCells.filter((cell) => cell.cellKind === CellKind.Code); + + if (notebookCells) { + const promises: Promise[] = []; + // limit the number of cells so that we don't resolve an excessive amount of text models + for (const cell of notebookCells.slice(0, 100)) { + // gather all symbols asynchronously + promises.push(this._outlineEntryFactory.cacheSymbols(cell, this._outlineModelService, cancelToken)); + } + await Promise.allSettled(promises); + } + this.recomputeState(); + } + + public recomputeState(): void { + this._disposables.clear(); + this._activeEntry = undefined; + this._uri = undefined; + + if (!this._editor.hasModel()) { + return; + } + + this._uri = this._editor.textModel.uri; + + const notebookEditorWidget: IActiveNotebookEditor = this._editor; + + if (notebookEditorWidget.getLength() === 0) { + return; + } + + const notebookCells = notebookEditorWidget.getViewModel().viewCells; + + const entries: OutlineEntry[] = []; + for (const cell of notebookCells) { + entries.push(...this._outlineEntryFactory.getOutlineEntries(cell, entries.length)); + } + + // build a tree from the list of entries + if (entries.length > 0) { + const result: OutlineEntry[] = [entries[0]]; + const parentStack: OutlineEntry[] = [entries[0]]; + + for (let i = 1; i < entries.length; i++) { + const entry = entries[i]; + + while (true) { + const len = parentStack.length; + if (len === 0) { + // root node + result.push(entry); + parentStack.push(entry); + break; + + } else { + const parentCandidate = parentStack[len - 1]; + if (parentCandidate.level < entry.level) { + parentCandidate.addChild(entry); + parentStack.push(entry); + break; + } else { + parentStack.pop(); + } + } + } + } + this._entries = result; + } + + // feature: show markers with each cell + const markerServiceListener = new MutableDisposable(); + this._disposables.add(markerServiceListener); + const updateMarkerUpdater = () => { + if (notebookEditorWidget.isDisposed) { + return; + } + + const doUpdateMarker = (clear: boolean) => { + for (const entry of this._entries) { + if (clear) { + entry.clearMarkers(); + } else { + entry.updateMarkers(this._markerService); + } + } + }; + const problem = this._configurationService.getValue('problems.visibility'); + if (problem === undefined) { + return; + } + + const config = this._configurationService.getValue(OutlineConfigKeys.problemsEnabled); + + if (problem && config) { + markerServiceListener.value = this._markerService.onMarkerChanged(e => { + if (notebookEditorWidget.isDisposed) { + console.error('notebook editor is disposed'); + return; + } + + if (e.some(uri => notebookEditorWidget.getCellsInRange().some(cell => isEqual(cell.uri, uri)))) { + doUpdateMarker(false); + this._onDidChange.fire({}); + } + }); + doUpdateMarker(false); + } else { + markerServiceListener.clear(); + doUpdateMarker(true); + } + }; + updateMarkerUpdater(); + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('problems.visibility') || e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { + updateMarkerUpdater(); + this._onDidChange.fire({}); + } + })); + + const { changeEventTriggered } = this.recomputeActive(); + if (!changeEventTriggered) { + this._onDidChange.fire({}); + } + } + + public recomputeActive(): { changeEventTriggered: boolean } { + let newActive: OutlineEntry | undefined; + const notebookEditorWidget = this._editor; + + if (notebookEditorWidget) {//TODO don't check for widget, only here if we do have + if (notebookEditorWidget.hasModel() && notebookEditorWidget.getLength() > 0) { + const cell = notebookEditorWidget.cellAt(notebookEditorWidget.getFocus().start); + if (cell) { + for (const entry of this._entries) { + newActive = entry.find(cell, []); + if (newActive) { + break; + } + } + } + } + } + + if (newActive !== this._activeEntry) { + this._activeEntry = newActive; + this._onDidChange.fire({ affectOnlyActiveElement: true }); + return { changeEventTriggered: true }; + } + return { changeEventTriggered: false }; + } + + dispose(): void { + this._entries.length = 0; + this._activeEntry = undefined; + this._disposables.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory.ts new file mode 100644 index 00000000000..c4d2f82593a --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ReferenceCollection, type IReference } from 'vs/base/common/lifecycle'; +import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import type { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookCellOutlineDataSource } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource'; + +class NotebookCellOutlineDataSourceReferenceCollection extends ReferenceCollection { + constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { + super(); + } + protected override createReferencedObject(_key: string, editor: INotebookEditor): NotebookCellOutlineDataSource { + return this.instantiationService.createInstance(NotebookCellOutlineDataSource, editor); + } + protected override destroyReferencedObject(_key: string, object: NotebookCellOutlineDataSource): void { + object.dispose(); + } +} + +export const INotebookCellOutlineDataSourceFactory = createDecorator('INotebookCellOutlineDataSourceFactory'); + +export interface INotebookCellOutlineDataSourceFactory { + getOrCreate(editor: INotebookEditor): IReference; +} + +export class NotebookCellOutlineDataSourceFactory implements INotebookCellOutlineDataSourceFactory { + private readonly _data: NotebookCellOutlineDataSourceReferenceCollection; + constructor(@IInstantiationService instantiationService: IInstantiationService) { + this._data = instantiationService.createInstance(NotebookCellOutlineDataSourceReferenceCollection); + } + + getOrCreate(editor: INotebookEditor): IReference { + return this._data.acquire(editor.getId(), editor); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts index 77a8ba22ab4..a7de8857584 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts @@ -14,7 +14,6 @@ import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { IRange } from 'vs/editor/common/core/range'; import { SymbolKind } from 'vs/editor/common/languages'; -import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; export const enum NotebookOutlineConstants { NonHeaderOutlineLevel = 7, @@ -50,7 +49,7 @@ export class NotebookOutlineEntryFactory { private readonly executionStateService: INotebookExecutionStateService ) { } - public getOutlineEntries(cell: ICellViewModel, target: OutlineTarget, index: number): OutlineEntry[] { + public getOutlineEntries(cell: ICellViewModel, index: number): OutlineEntry[] { const entries: OutlineEntry[] = []; const isMarkdown = cell.cellKind === CellKind.Markup; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts deleted file mode 100644 index 1ac843597ab..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts +++ /dev/null @@ -1,316 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import { isEqual } from 'vs/base/common/resources'; -import { URI } from 'vs/base/common/uri'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IMarkerService } from 'vs/platform/markers/common/markers'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IActiveNotebookEditor, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, NotebookCellsChangeType, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { OutlineChangeEvent, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; -import { OutlineEntry } from './OutlineEntry'; -import { IOutlineModelService } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { NotebookOutlineConstants, NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; -import { Delayer } from 'vs/base/common/async'; - -export class NotebookCellOutlineProvider { - private readonly _disposables = new DisposableStore(); - private readonly _onDidChange = new Emitter(); - - readonly onDidChange: Event = this._onDidChange.event; - - private _uri: URI | undefined; - private _entries: OutlineEntry[] = []; - get entries(): OutlineEntry[] { - if (this.delayedOutlineRecompute.isTriggered()) { - this.delayedOutlineRecompute.cancel(); - this._recomputeState(); - } - return this._entries; - } - - private _activeEntry?: OutlineEntry; - private readonly _entriesDisposables = new DisposableStore(); - - readonly outlineKind = 'notebookCells'; - - get activeElement(): OutlineEntry | undefined { - if (this.delayedOutlineRecompute.isTriggered()) { - this.delayedOutlineRecompute.cancel(); - this._recomputeState(); - } - return this._activeEntry; - } - - private readonly _outlineEntryFactory: NotebookOutlineEntryFactory; - private readonly delayedOutlineRecompute: Delayer; - constructor( - private readonly _editor: INotebookEditor, - private readonly _target: OutlineTarget, - @IThemeService themeService: IThemeService, - @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, - @IOutlineModelService private readonly _outlineModelService: IOutlineModelService, - @IMarkerService private readonly _markerService: IMarkerService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - ) { - this._outlineEntryFactory = new NotebookOutlineEntryFactory(notebookExecutionStateService); - - const delayerRecomputeActive = this._disposables.add(new Delayer(200)); - this._disposables.add(_editor.onDidChangeSelection(() => { - delayerRecomputeActive.trigger(() => this._recomputeActive()); - }, this)); - - // .3s of a delay is sufficient, 100-200s is too quick and will unnecessarily block the ui thread. - // Given we're only updating the outline when the user types, we can afford to wait a bit. - this.delayedOutlineRecompute = this._disposables.add(new Delayer(300)); - const delayedRecompute = () => { - delayerRecomputeActive.cancel(); // Active is always recomputed after a recomputing the outline state. - this.delayedOutlineRecompute.trigger(() => this._recomputeState()); - }; - - this._disposables.add(_configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(NotebookSetting.outlineShowMarkdownHeadersOnly) || - e.affectsConfiguration(NotebookSetting.outlineShowCodeCells) || - e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols) || - e.affectsConfiguration(NotebookSetting.breadcrumbsShowCodeCells) - ) { - delayedRecompute(); - } - })); - - this._disposables.add(themeService.onDidFileIconThemeChange(() => { - this._onDidChange.fire({}); - })); - - this._disposables.add( - notebookExecutionStateService.onDidChangeExecution(e => { - if (e.type === NotebookExecutionType.cell && !!this._editor.textModel && e.affectsNotebook(this._editor.textModel?.uri)) { - delayedRecompute(); - } - }) - ); - - const disposable = this._disposables.add(new DisposableStore()); - const monitorModelChanges = () => { - disposable.clear(); - if (!this._editor.textModel) { - return; - } - disposable.add(this._editor.textModel.onDidChangeContent(contentChanges => { - if (contentChanges.rawEvents.some(c => c.kind === NotebookCellsChangeType.ChangeCellContent || - c.kind === NotebookCellsChangeType.ChangeCellInternalMetadata || - c.kind === NotebookCellsChangeType.Move || - c.kind === NotebookCellsChangeType.ModelChange)) { - delayedRecompute(); - } - })); - // Perhaps this is the first time we're building the outline - if (!this._entries.length) { - this._recomputeState(); - } - }; - this._disposables.add(this._editor.onDidChangeModel(monitorModelChanges)); - monitorModelChanges(); - this._recomputeState(); - } - - dispose(): void { - this._entries.length = 0; - this._activeEntry = undefined; - this._entriesDisposables.dispose(); - this._disposables.dispose(); - } - - async setFullSymbols(cancelToken: CancellationToken) { - const notebookEditorWidget = this._editor; - - const notebookCells = notebookEditorWidget?.getViewModel()?.viewCells.filter((cell) => cell.cellKind === CellKind.Code); - - if (notebookCells) { - const promises: Promise[] = []; - // limit the number of cells so that we don't resolve an excessive amount of text models - for (const cell of notebookCells.slice(0, 100)) { - // gather all symbols asynchronously - promises.push(this._outlineEntryFactory.cacheSymbols(cell, this._outlineModelService, cancelToken)); - } - await Promise.allSettled(promises); - } - - this._recomputeState(); - } - private _recomputeState(): void { - this._entriesDisposables.clear(); - this._activeEntry = undefined; - this._uri = undefined; - - if (!this._editor.hasModel()) { - return; - } - - this._uri = this._editor.textModel.uri; - - const notebookEditorWidget: IActiveNotebookEditor = this._editor; - - if (notebookEditorWidget.getLength() === 0) { - return; - } - - let includeCodeCells = true; - if (this._target === OutlineTarget.Breadcrumbs) { - includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); - } - - let notebookCells: ICellViewModel[]; - if (this._target === OutlineTarget.Breadcrumbs) { - notebookCells = notebookEditorWidget.getViewModel().viewCells.filter((cell) => cell.cellKind === CellKind.Markup || includeCodeCells); - } else { - notebookCells = notebookEditorWidget.getViewModel().viewCells; - } - - const entries: OutlineEntry[] = []; - for (const cell of notebookCells) { - entries.push(...this._outlineEntryFactory.getOutlineEntries(cell, this._target, entries.length)); - } - - // build a tree from the list of entries - if (entries.length > 0) { - const result: OutlineEntry[] = [entries[0]]; - const parentStack: OutlineEntry[] = [entries[0]]; - - for (let i = 1; i < entries.length; i++) { - const entry = entries[i]; - - while (true) { - const len = parentStack.length; - if (len === 0) { - // root node - result.push(entry); - parentStack.push(entry); - break; - - } else { - const parentCandidate = parentStack[len - 1]; - if (parentCandidate.level < entry.level) { - parentCandidate.addChild(entry); - parentStack.push(entry); - break; - } else { - parentStack.pop(); - } - } - } - } - this._entries = result; - } - - // feature: show markers with each cell - const markerServiceListener = new MutableDisposable(); - this._entriesDisposables.add(markerServiceListener); - const updateMarkerUpdater = () => { - if (notebookEditorWidget.isDisposed) { - return; - } - - const doUpdateMarker = (clear: boolean) => { - for (const entry of this._entries) { - if (clear) { - entry.clearMarkers(); - } else { - entry.updateMarkers(this._markerService); - } - } - }; - const problem = this._configurationService.getValue('problems.visibility'); - if (problem === undefined) { - return; - } - - const config = this._configurationService.getValue(OutlineConfigKeys.problemsEnabled); - - if (problem && config) { - markerServiceListener.value = this._markerService.onMarkerChanged(e => { - if (notebookEditorWidget.isDisposed) { - console.error('notebook editor is disposed'); - return; - } - - if (e.some(uri => notebookEditorWidget.getCellsInRange().some(cell => isEqual(cell.uri, uri)))) { - doUpdateMarker(false); - this._onDidChange.fire({}); - } - }); - doUpdateMarker(false); - } else { - markerServiceListener.clear(); - doUpdateMarker(true); - } - }; - updateMarkerUpdater(); - this._entriesDisposables.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('problems.visibility') || e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { - updateMarkerUpdater(); - this._onDidChange.fire({}); - } - })); - - const { changeEventTriggered } = this._recomputeActive(); - if (!changeEventTriggered) { - this._onDidChange.fire({}); - } - } - - private _recomputeActive(): { changeEventTriggered: boolean } { - let newActive: OutlineEntry | undefined; - const notebookEditorWidget = this._editor; - - if (notebookEditorWidget) {//TODO don't check for widget, only here if we do have - if (notebookEditorWidget.hasModel() && notebookEditorWidget.getLength() > 0) { - const cell = notebookEditorWidget.cellAt(notebookEditorWidget.getFocus().start); - if (cell) { - for (const entry of this._entries) { - newActive = entry.find(cell, []); - if (newActive) { - break; - } - } - } - } - } - - // @Yoyokrazy - Make sure the new active entry isn't part of the filtered exclusions - const showCodeCells = this._configurationService.getValue(NotebookSetting.outlineShowCodeCells); - const showCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); - const showMarkdownHeadersOnly = this._configurationService.getValue(NotebookSetting.outlineShowMarkdownHeadersOnly); - - // check the three outline filtering conditions - // if any are true, newActive should NOT be set to this._activeEntry and the event should NOT fire - if ( - (newActive !== this._activeEntry) && !( - (showMarkdownHeadersOnly && newActive?.cell.cellKind === CellKind.Markup && newActive?.level === NotebookOutlineConstants.NonHeaderOutlineLevel) || // show headers only + cell is mkdn + is level 7 (no header) - (!showCodeCells && newActive?.cell.cellKind === CellKind.Code) || // show code cells + cell is code - (!showCodeCellSymbols && newActive?.cell.cellKind === CellKind.Code && newActive?.level > NotebookOutlineConstants.NonHeaderOutlineLevel) // show code symbols + cell is code + has level > 7 (nb symbol levels) - ) - ) { - this._activeEntry = newActive; - this._onDidChange.fire({ affectOnlyActiveElement: true }); - return { changeEventTriggered: true }; - } - - return { changeEventTriggered: false }; - } - - get isEmpty(): boolean { - return this._entries.length === 0; - } - - get uri() { - return this._uri; - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory.ts deleted file mode 100644 index 54411bcd296..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory.ts +++ /dev/null @@ -1,39 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ReferenceCollection, type IReference } from 'vs/base/common/lifecycle'; -import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import type { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; -import type { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; - -class NotebookCellOutlineProviderReferenceCollection extends ReferenceCollection { - constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { - super(); - } - protected override createReferencedObject(_key: string, editor: INotebookEditor, target: OutlineTarget): NotebookCellOutlineProvider { - return this.instantiationService.createInstance(NotebookCellOutlineProvider, editor, target); - } - protected override destroyReferencedObject(_key: string, object: NotebookCellOutlineProvider): void { - object.dispose(); - } -} - -export const INotebookCellOutlineProviderFactory = createDecorator('INotebookCellOutlineProviderFactory'); - -export interface INotebookCellOutlineProviderFactory { - getOrCreate(editor: INotebookEditor, target: OutlineTarget): IReference; -} - -export class NotebookCellOutlineProviderFactory implements INotebookCellOutlineProviderFactory { - private readonly _data: NotebookCellOutlineProviderReferenceCollection; - constructor(@IInstantiationService instantiationService: IInstantiationService) { - this._data = instantiationService.createInstance(NotebookCellOutlineProviderReferenceCollection); - } - - getOrCreate(editor: INotebookEditor, target: OutlineTarget): IReference { - return this._data.acquire(editor.getId(), editor, target); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index b7fa0bc7ecf..60a3626484f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -13,27 +13,27 @@ import { URI } from 'vs/base/common/uri'; import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IWorkspaceTextEdit } from 'vs/editor/common/languages'; import { FindMatch, IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { MultiModelEditStackElement, SingleModelEditStackElement } from 'vs/editor/common/model/editStack'; import { IntervalNode, IntervalTree } from 'vs/editor/common/model/intervalTree'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { IWorkspaceTextEdit } from 'vs/editor/common/languages'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { FoldingRegions } from 'vs/editor/contrib/folding/browser/foldingRanges'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { CellEditState, CellFindMatchWithIndex, CellFoldingState, EditorFoldingStateDelegate, ICellViewModel, INotebookDeltaCellStatusBarItems, INotebookDeltaDecoration, ICellModelDecorations, ICellModelDeltaDecorations, IModelDecorationsChangeAccessor, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellFindMatchModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; +import { CellEditState, CellFindMatchWithIndex, CellFoldingState, EditorFoldingStateDelegate, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, IModelDecorationsChangeAccessor, INotebookDeltaCellStatusBarItems, INotebookDeltaDecoration, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookLayoutInfo, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { NotebookCellSelectionCollection } from 'vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, ICell, INotebookSearchOptions, ISelectionState, NotebookCellsChangeType, NotebookCellTextModelSplice, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { cellIndexesToRanges, cellRangesToIndexes, ICellRange, reduceCellRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { NotebookLayoutInfo, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { CellFindMatchModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; +import { CellKind, ICell, INotebookFindOptions, ISelectionState, NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookFindScopeType, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { cellIndexesToRanges, cellRangesToIndexes, ICellRange, reduceCellRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; const invalidFunc = () => { throw new Error(`Invalid change accessor`); }; @@ -99,6 +99,7 @@ let MODEL_ID = 0; export interface NotebookViewModelOptions { isReadOnly: boolean; + inRepl?: boolean; } export class NotebookViewModel extends Disposable implements EditorFoldingStateDelegate, INotebookViewModel { @@ -108,15 +109,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private readonly _onDidChangeOptions = this._register(new Emitter()); get onDidChangeOptions(): Event { return this._onDidChangeOptions.event; } private _viewCells: CellViewModel[] = []; + private readonly replView: boolean; get viewCells(): ICellViewModel[] { return this._viewCells; } - set viewCells(_: ICellViewModel[]) { - throw new Error('NotebookViewModel.viewCells is readonly'); - } - get length(): number { return this._viewCells.length; } @@ -206,6 +204,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD MODEL_ID++; this.id = '$notebookViewModel' + MODEL_ID; this._instanceId = strings.singleLetterHash(MODEL_ID); + this.replView = !!this.options.inRepl; const compute = (changes: NotebookCellTextModelSplice[], synchronous: boolean) => { const diffs = changes.map(splice => { @@ -337,9 +336,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._onDidChangeSelection.fire(e); })); - this._viewCells = this._notebook.cells.map(cell => { - return createCellViewModel(this._instantiationService, this, cell, this._viewContext); - }); + + const viewCellCount = this.replView ? this._notebook.cells.length - 1 : this._notebook.cells.length; + for (let i = 0; i < viewCellCount; i++) { + this._viewCells.push(createCellViewModel(this._instantiationService, this, this._notebook.cells[i], this._viewContext)); + } + this._viewCells.forEach(cell => { this._handleToViewCellMapping.set(cell.handle, cell); @@ -908,9 +910,19 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } //#region Find - find(value: string, options: INotebookSearchOptions): CellFindMatchWithIndex[] { + find(value: string, options: INotebookFindOptions): CellFindMatchWithIndex[] { const matches: CellFindMatchWithIndex[] = []; - this._viewCells.forEach((cell, index) => { + let findCells: CellViewModel[] = []; + + if (options.findScope && (options.findScope.findScopeType === NotebookFindScopeType.Cells || options.findScope.findScopeType === NotebookFindScopeType.Text)) { + const selectedRanges = options.findScope.selectedCellRanges?.map(range => this.validateRange(range)).filter(range => !!range) ?? []; + const selectedIndexes = cellRangesToIndexes(selectedRanges); + findCells = selectedIndexes.map(index => this._viewCells[index]); + } else { + findCells = this._viewCells; + } + + findCells.forEach((cell, index) => { const cellMatches = cell.startFind(value, options); if (cellMatches) { matches.push(new CellFindMatchModel( diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index 52e6889c4bc..50a6758941b 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts @@ -13,7 +13,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { CellFoldingState, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry'; -import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; +import { NotebookCellOutlineDataSource } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Delayer } from 'vs/base/common/async'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -23,8 +23,7 @@ import { FoldingController } from 'vs/workbench/contrib/notebook/browser/control import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { NotebookSectionArgs } from 'vs/workbench/contrib/notebook/browser/controller/sectionActions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory'; -import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; +import { INotebookCellOutlineDataSourceFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory'; export class NotebookStickyLine extends Disposable { constructor( @@ -105,7 +104,7 @@ export class NotebookStickyScroll extends Disposable { private readonly _onDidChangeNotebookStickyScroll = this._register(new Emitter()); readonly onDidChangeNotebookStickyScroll: Event = this._onDidChangeNotebookStickyScroll.event; - private notebookOutlineReference?: IReference; + private notebookCellOutlineReference?: IReference; getDomNode(): HTMLElement { return this.domNode; @@ -191,37 +190,37 @@ export class NotebookStickyScroll extends Disposable { this.init(); } else { this._disposables.clear(); - this.notebookOutlineReference?.dispose(); + this.notebookCellOutlineReference?.dispose(); this.disposeCurrentStickyLines(); DOM.clearNode(this.domNode); this.updateDisplay(); } - } else if (e.stickyScrollMode && this.notebookEditor.notebookOptions.getDisplayOptions().stickyScrollEnabled && this.notebookOutlineReference?.object) { - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, this.notebookOutlineReference?.object?.entries, this.getCurrentStickyHeight())); + } else if (e.stickyScrollMode && this.notebookEditor.notebookOptions.getDisplayOptions().stickyScrollEnabled && this.notebookCellOutlineReference?.object) { + this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, this.notebookCellOutlineReference?.object?.entries, this.getCurrentStickyHeight())); } } private init() { - const { object: notebookOutlineReference } = this.notebookOutlineReference = this.instantiationService.invokeFunction((accessor) => accessor.get(INotebookCellOutlineProviderFactory).getOrCreate(this.notebookEditor, OutlineTarget.OutlinePane)); - this._register(this.notebookOutlineReference); - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookOutlineReference.entries, this.getCurrentStickyHeight())); + const { object: notebookCellOutline } = this.notebookCellOutlineReference = this.instantiationService.invokeFunction((accessor) => accessor.get(INotebookCellOutlineDataSourceFactory).getOrCreate(this.notebookEditor)); + this._register(this.notebookCellOutlineReference); + this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight())); - this._disposables.add(notebookOutlineReference.onDidChange(() => { - const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookOutlineReference.entries, this.getCurrentStickyHeight()); + this._disposables.add(notebookCellOutline.onDidChange(() => { + const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); if (!this.compareStickyLineMaps(recompute, this.currentStickyLines)) { this.updateContent(recompute); } })); this._disposables.add(this.notebookEditor.onDidAttachViewModel(() => { - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookOutlineReference.entries, this.getCurrentStickyHeight())); + this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight())); })); this._disposables.add(this.notebookEditor.onDidScroll(() => { const d = new Delayer(100); d.trigger(() => { d.dispose(); - const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookOutlineReference.entries, this.getCurrentStickyHeight()); + const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); if (!this.compareStickyLineMaps(recompute, this.currentStickyLines)) { this.updateContent(recompute); } @@ -369,7 +368,7 @@ export class NotebookStickyScroll extends Disposable { override dispose() { this._disposables.dispose(); this.disposeCurrentStickyLines(); - this.notebookOutlineReference?.dispose(); + this.notebookCellOutlineReference?.dispose(); super.dispose(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index 6348c0d359f..ea1039d672c 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -416,7 +416,7 @@ export class NotebookEditorWorkbenchToolbar extends Disposable { this._renderLabel = this._convertConfiguration(this.configurationService.getValue(NotebookSetting.globalToolbarShowLabel)); this._updateStrategy(); const oldElement = this._notebookLeftToolbar.getElement(); - oldElement.parentElement?.removeChild(oldElement); + oldElement.remove(); this._notebookLeftToolbar.dispose(); this._notebookLeftToolbar = this.instantiationService.createInstance( diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 25feb7f9123..8874848aabb 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -384,13 +384,13 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { description: suggestedExtension.displayName ?? suggestedExtension.extensionIds.join(', '), label: `$(${Codicon.lightbulb.id}) ` + localize('installSuggestedKernel', 'Install/Enable suggested extensions'), extensionIds: suggestedExtension.extensionIds - } as InstallExtensionPick); + } satisfies InstallExtensionPick); } // there is no kernel, show the install from marketplace quickPickItems.push({ id: 'install', label: localize('searchForKernels', "Browse marketplace for kernel extensions"), - } as SearchMarketplacePick); + } satisfies SearchMarketplacePick); return quickPickItems; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts index 9856cbf900b..19b4df7f193 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts @@ -113,9 +113,11 @@ export class ListTopCellToolbar extends Disposable { hiddenItemStrategy: HiddenItemStrategy.Ignore, }); - toolbar.context = { - notebookEditor: this.notebookEditor - }; + if (this.notebookEditor.hasModel()) { + toolbar.context = { + notebookEditor: this.notebookEditor + } satisfies INotebookActionContext; + } this.viewZone.value?.add(toolbar); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index b3b6ff6e2f9..21870a0067f 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -215,6 +215,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return this._alternativeVersionId; } + get notebookType() { + return this.viewType; + } + constructor( readonly viewType: string, readonly uri: URI, diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 7828091e598..aea2596da48 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -8,36 +8,41 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDiffResult } from 'vs/base/common/diff/diff'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { ISplice } from 'vs/base/common/sequence'; +import { ThemeColor } from 'vs/base/common/themables'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { Range } from 'vs/editor/common/core/range'; import { ILineChange } from 'vs/editor/common/diff/legacyLinesDiffComputer'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { Command, WorkspaceEditMetadata } from 'vs/editor/common/languages'; import { IReadonlyTextBuffer } from 'vs/editor/common/model'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { ThemeColor } from 'vs/base/common/themables'; +import { IFileReadLimits } from 'vs/platform/files/common/files'; import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { IWorkingCopyBackupMeta, IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { IFileReadLimits } from 'vs/platform/files/common/files'; -import { parse as parseUri, generate as generateUri } from 'vs/workbench/services/notebook/common/notebookDocumentService'; import { ICellExecutionError } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { INotebookTextModelLike } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { generate as generateUri, parse as parseUri } from 'vs/workbench/services/notebook/common/notebookDocumentService'; +import { IWorkingCopyBackupMeta, IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; export const NOTEBOOK_EDITOR_ID = 'workbench.editor.notebook'; export const NOTEBOOK_DIFF_EDITOR_ID = 'workbench.editor.notebookTextDiffEditor'; export const INTERACTIVE_WINDOW_EDITOR_ID = 'workbench.editor.interactive'; +export const REPL_EDITOR_ID = 'workbench.editor.repl'; +export const EXECUTE_REPL_COMMAND_ID = 'replNotebook.input.execute'; export enum CellKind { Markup = 1, @@ -252,7 +257,8 @@ export interface ICell { onDidChangeInternalMetadata: Event; } -export interface INotebookTextModel { +export interface INotebookTextModel extends INotebookTextModelLike { + readonly notebookType: string; readonly viewType: string; metadata: NotebookDocumentMetadata; readonly transientOptions: TransientOptions; @@ -551,7 +557,7 @@ export interface INotebookContributionData { providerDisplayName: string; displayName: string; filenamePattern: (string | glob.IRelativePattern | INotebookExclusiveDocumentFilter)[]; - exclusive: boolean; + priority?: RegisteredEditorPriority; } @@ -776,6 +782,11 @@ export interface INotebookLoadOptions { readonly limits?: IFileReadLimits; } +export type NotebookEditorModelCreationOptions = { + limits?: IFileReadLimits; + scratchpad?: boolean; +}; + export interface IResolvedNotebookEditorModel extends INotebookEditorModel { notebook: NotebookTextModel; } @@ -818,7 +829,7 @@ export enum NotebookEditorPriority { option = 'option', } -export interface INotebookSearchOptions { +export interface INotebookFindOptions { regex?: boolean; wholeWord?: boolean; caseSensitive?: boolean; @@ -827,6 +838,19 @@ export interface INotebookSearchOptions { includeMarkupPreview?: boolean; includeCodeInput?: boolean; includeOutput?: boolean; + findScope?: INotebookFindScope; +} + +export interface INotebookFindScope { + findScopeType: NotebookFindScopeType; + selectedCellRanges?: ICellRange[]; + selectedTextRanges?: Range[]; +} + +export enum NotebookFindScopeType { + Cells = 'cells', + Text = 'text', + None = 'none' } export interface INotebookExclusiveDocumentFilter { @@ -951,7 +975,7 @@ export const NotebookSetting = { outputFontSize: 'notebook.output.fontSize', outputFontFamilyDeprecated: 'notebook.outputFontFamily', outputFontFamily: 'notebook.output.fontFamily', - findScope: 'notebook.find.scope', + findFilters: 'notebook.find.filters', logging: 'notebook.logging', confirmDeleteRunningCell: 'notebook.confirmDeleteRunningCell', remoteSaving: 'notebook.experimental.remoteSave', diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index 4aad255f787..e659674d527 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { INTERACTIVE_WINDOW_EDITOR_ID, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INTERACTIVE_WINDOW_EDITOR_ID, NOTEBOOK_EDITOR_ID, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -16,11 +16,15 @@ export const InteractiveWindowOpen = new RawContextKey('interactiveWind // Is Notebook export const NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', NOTEBOOK_EDITOR_ID); export const INTERACTIVE_WINDOW_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', INTERACTIVE_WINDOW_EDITOR_ID); +export const REPL_NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', REPL_EDITOR_ID); // Editor keys +// based on the focus of the notebook editor widget export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey('notebookEditorFocused', false); +// always true within the cell list html element export const NOTEBOOK_CELL_LIST_FOCUSED = new RawContextKey('notebookCellListFocused', false); export const NOTEBOOK_OUTPUT_FOCUSED = new RawContextKey('notebookOutputFocused', false); +// an input html element within the output webview has focus export const NOTEBOOK_OUTPUT_INPUT_FOCUSED = new RawContextKey('notebookOutputInputFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey('notebookEditable', true); export const NOTEBOOK_HAS_RUNNING_CELL = new RawContextKey('notebookHasRunningCell', false); @@ -43,6 +47,8 @@ export type NotebookCellExecutionStateContext = 'idle' | 'pending' | 'executing' export const NOTEBOOK_CELL_EXECUTION_STATE = new RawContextKey('notebookCellExecutionState', undefined); export const NOTEBOOK_CELL_EXECUTING = new RawContextKey('notebookCellExecuting', false); // This only exists to simplify a context key expression, see #129625 export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey('notebookCellHasOutputs', false); +export const NOTEBOOK_CELL_IS_FIRST_OUTPUT = new RawContextKey('notebookCellIsFirstOutput', false); +export const NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS = new RawContextKey('hasHiddenOutputs', false); export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('notebookCellInputIsCollapsed', false); export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); export const NOTEBOOK_CELL_RESOURCE = new RawContextKey('notebookCellResource', ''); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 0ac6de71100..5edcb35f62c 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -53,7 +53,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { static readonly ID: string = 'workbench.input.notebook'; - private _editorModelReference: IReference | null = null; + protected editorModelReference: IReference | null = null; private _sideLoadedListener: IDisposable; private _defaultDirtyState: boolean = false; @@ -105,8 +105,8 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { override dispose() { this._sideLoadedListener.dispose(); - this._editorModelReference?.dispose(); - this._editorModelReference = null; + this.editorModelReference?.dispose(); + this.editorModelReference = null; super.dispose(); } @@ -125,8 +125,8 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { capabilities |= EditorInputCapabilities.Untitled; } - if (this._editorModelReference) { - if (this._editorModelReference.object.isReadonly()) { + if (this.editorModelReference) { + if (this.editorModelReference.object.isReadonly()) { capabilities |= EditorInputCapabilities.Readonly; } } else { @@ -143,7 +143,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override getDescription(verbosity = Verbosity.MEDIUM): string | undefined { - if (!this.hasCapability(EditorInputCapabilities.Untitled) || this._editorModelReference?.object.hasAssociatedFilePath()) { + if (!this.hasCapability(EditorInputCapabilities.Untitled) || this.editorModelReference?.object.hasAssociatedFilePath()) { return super.getDescription(verbosity); } @@ -151,21 +151,21 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override isReadonly(): boolean | IMarkdownString { - if (!this._editorModelReference) { + if (!this.editorModelReference) { return this.filesConfigurationService.isReadonly(this.resource); } - return this._editorModelReference.object.isReadonly(); + return this.editorModelReference.object.isReadonly(); } override isDirty() { - if (!this._editorModelReference) { + if (!this.editorModelReference) { return this._defaultDirtyState; } - return this._editorModelReference.object.isDirty(); + return this.editorModelReference.object.isDirty(); } override isSaving(): boolean { - const model = this._editorModelReference?.object; + const model = this.editorModelReference?.object; if (!model || !model.isDirty() || model.hasErrorState || this.hasCapability(EditorInputCapabilities.Untitled)) { return false; // require the model to be dirty, file-backed and not in an error state } @@ -175,12 +175,12 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override async save(group: GroupIdentifier, options?: ISaveOptions): Promise { - if (this._editorModelReference) { + if (this.editorModelReference) { if (this.hasCapability(EditorInputCapabilities.Untitled)) { return this.saveAs(group, options); } else { - await this._editorModelReference.object.save(options); + await this.editorModelReference.object.save(options); } return this; @@ -190,7 +190,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { - if (!this._editorModelReference) { + if (!this.editorModelReference) { return undefined; } @@ -200,9 +200,9 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return undefined; } - const pathCandidate = this.hasCapability(EditorInputCapabilities.Untitled) ? await this._suggestName(provider, this.labelService.getUriBasenameLabel(this.resource)) : this._editorModelReference.object.resource; + const pathCandidate = this.hasCapability(EditorInputCapabilities.Untitled) ? await this._suggestName(provider, this.labelService.getUriBasenameLabel(this.resource)) : this.editorModelReference.object.resource; let target: URI | undefined; - if (this._editorModelReference.object.hasAssociatedFilePath()) { + if (this.editorModelReference.object.hasAssociatedFilePath()) { target = pathCandidate; } else { target = await this._fileDialogService.pickFileToSave(pathCandidate, options?.availableFileSystems); @@ -231,7 +231,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { throw new Error(`File name ${target} is not supported by ${provider.providerDisplayName}.\n\nPlease make sure the file name matches following patterns:\n${patterns}`); } - return await this._editorModelReference.object.saveAs(target); + return await this.editorModelReference.object.saveAs(target); } private async _suggestName(provider: NotebookProviderInfo, suggestedFilename: string) { @@ -260,7 +260,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { // called when users rename a notebook document override async rename(group: GroupIdentifier, target: URI): Promise { - if (this._editorModelReference) { + if (this.editorModelReference) { return { editor: { resource: target }, options: { override: this.viewType } }; } @@ -268,8 +268,8 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override async revert(_group: GroupIdentifier, options?: IRevertOptions): Promise { - if (this._editorModelReference && this._editorModelReference.object.isDirty()) { - await this._editorModelReference.object.revert(options); + if (this.editorModelReference && this.editorModelReference.object.isDirty()) { + await this.editorModelReference.object.revert(options); } } @@ -284,42 +284,43 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { // "other" loading anymore this._sideLoadedListener.dispose(); - if (!this._editorModelReference) { - const ref = await this._notebookModelResolverService.resolve(this.resource, this.viewType, this.ensureLimits(_options)); - if (this._editorModelReference) { + if (!this.editorModelReference) { + const scratchpad = this.capabilities & EditorInputCapabilities.Scratchpad ? true : false; + const ref = await this._notebookModelResolverService.resolve(this.resource, this.viewType, { limits: this.ensureLimits(_options), scratchpad }); + if (this.editorModelReference) { // Re-entrant, double resolve happened. Dispose the addition references and proceed // with the truth. ref.dispose(); - return (>this._editorModelReference).object; + return (>this.editorModelReference).object; } - this._editorModelReference = ref; + this.editorModelReference = ref; if (this.isDisposed()) { - this._editorModelReference.dispose(); - this._editorModelReference = null; + this.editorModelReference.dispose(); + this.editorModelReference = null; return null; } - this._register(this._editorModelReference.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this._register(this._editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); - this._register(this._editorModelReference.object.onDidRevertUntitled(() => this.dispose())); - if (this._editorModelReference.object.isDirty()) { + this._register(this.editorModelReference.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + this._register(this.editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); + this._register(this.editorModelReference.object.onDidRevertUntitled(() => this.dispose())); + if (this.editorModelReference.object.isDirty()) { this._onDidChangeDirty.fire(); } } else { - this._editorModelReference.object.load({ limits: this.ensureLimits(_options) }); + this.editorModelReference.object.load({ limits: this.ensureLimits(_options) }); } if (this.options._backupId) { - const info = await this._notebookService.withNotebookDataProvider(this._editorModelReference.object.notebook.viewType); + const info = await this._notebookService.withNotebookDataProvider(this.editorModelReference.object.notebook.viewType); if (!(info instanceof SimpleNotebookProviderInfo)) { throw new Error('CANNOT open file notebook with this provider'); } const data = await info.serializer.dataToNotebook(VSBuffer.fromString(JSON.stringify({ __webview_backup: this.options._backupId }))); - this._editorModelReference.object.notebook.applyEdits([ + this.editorModelReference.object.notebook.applyEdits([ { editType: CellEditType.Replace, index: 0, - count: this._editorModelReference.object.notebook.length, + count: this.editorModelReference.object.notebook.length, cells: data.cells } ], true, undefined, () => undefined, undefined, false); @@ -331,7 +332,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } } - return this._editorModelReference.object; + return this.editorModelReference.object; } override toUntyped(): IResourceEditorInput { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 3c26eff229a..a6a06d63186 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -15,6 +15,7 @@ import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWriteFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; @@ -197,7 +198,8 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF private readonly _notebookModel: NotebookTextModel, private readonly _notebookService: INotebookService, private readonly _configurationService: IConfigurationService, - private readonly _telemetryService: ITelemetryService + private readonly _telemetryService: ITelemetryService, + private readonly _logService: ILogService ) { super(); @@ -237,13 +239,22 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF } private async setSaveDelegate() { - const serializer = await this.getNotebookSerializer(); - this.save = async (options: IWriteFileOptions, token: CancellationToken) => { - if (token.isCancellationRequested) { - throw new CancellationError(); - } + // make sure we wait for a serializer to resolve before we try to handle saves in the EH + await this.getNotebookSerializer(); + this.save = async (options: IWriteFileOptions, token: CancellationToken) => { try { + let serializer = this._notebookService.tryGetDataProviderSync(this.notebookModel.viewType)?.serializer; + + if (!serializer) { + this._logService.warn('No serializer found for notebook model, checking if provider still needs to be resolved'); + serializer = await this.getNotebookSerializer(); + } + + if (token.isCancellationRequested) { + throw new CancellationError(); + } + const stat = await serializer.save(this._notebookModel.uri, this._notebookModel.versionId, options, token); return stat; } catch (error) { @@ -358,7 +369,8 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo private readonly _viewType: string, @INotebookService private readonly _notebookService: INotebookService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILogService private readonly _logService: ILogService ) { } async createModel(resource: URI, stream: VSBufferReadableStream, token: CancellationToken): Promise { @@ -376,7 +388,7 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo } const notebookModel = this._notebookService.createNotebookTextModel(info.viewType, resource, data, info.serializer.options); - return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService, this._telemetryService); + return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService, this._telemetryService, this._logService); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts index 3eff1eb5060..dc3d26a0ca3 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts @@ -5,10 +5,9 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IResolvedNotebookEditorModel, NotebookEditorModelCreationOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IReference } from 'vs/base/common/lifecycle'; import { Event, IWaitUntil } from 'vs/base/common/event'; -import { IFileReadLimits } from 'vs/platform/files/common/files'; export const INotebookEditorModelResolverService = createDecorator('INotebookModelResolverService'); @@ -50,6 +49,6 @@ export interface INotebookEditorModelResolverService { isDirty(resource: URI): boolean; - resolve(resource: URI, viewType?: string, limits?: IFileReadLimits): Promise>; - resolve(resource: IUntitledNotebookResource, viewType: string, limits?: IFileReadLimits): Promise>; + resolve(resource: URI, viewType?: string, creationOptions?: NotebookEditorModelCreationOptions): Promise>; + resolve(resource: IUntitledNotebookResource, viewType: string, creationOtions?: NotebookEditorModelCreationOptions): Promise>; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index 9d0a90e4576..d43273d4fa6 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -5,7 +5,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { CellUri, IResolvedNotebookEditorModel, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, IResolvedNotebookEditorModel, NotebookEditorModelCreationOptions, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -61,7 +61,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection { + protected async createReferencedObject(key: string, viewType: string, hasAssociatedFilePath: boolean, limits?: IFileReadLimits, isScratchpad?: boolean): Promise { // Untrack as being disposed this.modelsToDispose.delete(key); @@ -70,7 +70,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection>this._instantiationService.createInstance( FileWorkingCopyManager, workingCopyTypeId, @@ -79,8 +79,9 @@ class NotebookModelReferenceCollection extends ReferenceCollection(NotebookSetting.InteractiveWindowPromptToSave) !== true; - const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, hasAssociatedFilePath, viewType, workingCopyManager, scratchPad); + + const isScratchpadView = isScratchpad || (viewType === 'interactive' && this._configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true); + const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, hasAssociatedFilePath, viewType, workingCopyManager, isScratchpadView); const result = await model.load({ limits }); @@ -176,9 +177,9 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes return this._data.isDirty(resource); } - async resolve(resource: URI, viewType?: string, limits?: IFileReadLimits): Promise>; - async resolve(resource: IUntitledNotebookResource, viewType: string, limits?: IFileReadLimits): Promise>; - async resolve(arg0: URI | IUntitledNotebookResource, viewType?: string, limits?: IFileReadLimits): Promise> { + async resolve(resource: URI, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise>; + async resolve(resource: IUntitledNotebookResource, viewType: string, options: NotebookEditorModelCreationOptions): Promise>; + async resolve(arg0: URI | IUntitledNotebookResource, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise> { let resource: URI; let hasAssociatedFilePath = false; if (URI.isUri(arg0)) { @@ -219,8 +220,9 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes } else { await this._extensionService.whenInstalledExtensionsRegistered(); const providers = this._notebookService.getContributedNotebookTypes(resource); - const exclusiveProvider = providers.find(provider => provider.exclusive); - viewType = exclusiveProvider?.id || providers[0]?.id; + viewType = providers.find(provider => provider.priority === 'exclusive')?.id ?? + providers.find(provider => provider.priority === 'default')?.id ?? + providers[0]?.id; } } @@ -239,7 +241,7 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes } } - const reference = this._data.acquire(resource.toString(), viewType, hasAssociatedFilePath, limits); + const reference = this._data.acquire(resource.toString(), viewType, hasAssociatedFilePath, options?.limits, options?.scratchpad); try { const model = await reference.object; return { diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index be999203d75..2351e4b42b2 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -106,7 +106,7 @@ export interface IKernelSourceActionProvider { provideKernelSourceActions(): Promise; } -export interface INotebookTextModelLike { uri: URI; viewType: string } +export interface INotebookTextModelLike { uri: URI; notebookType: string } export const INotebookKernelService = createDecorator('INotebookKernelService'); diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts index 16a9ee3a57b..e2911dcf9e1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -19,7 +19,6 @@ export interface NotebookEditorDescriptor { readonly selectors: readonly { filenamePattern?: string; excludeFileNamePattern?: string }[]; readonly priority: RegisteredEditorPriority; readonly providerDisplayName: string; - readonly exclusive: boolean; } export class NotebookProviderInfo { @@ -29,7 +28,6 @@ export class NotebookProviderInfo { readonly displayName: string; readonly priority: RegisteredEditorPriority; readonly providerDisplayName: string; - readonly exclusive: boolean; private _selectors: NotebookSelector[]; get selectors() { @@ -50,7 +48,6 @@ export class NotebookProviderInfo { })) || []; this.priority = descriptor.priority; this.providerDisplayName = descriptor.providerDisplayName; - this.exclusive = descriptor.exclusive; this._options = { transientCellMetadata: {}, transientDocumentMetadata: {}, diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 7709674b937..50e49939035 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -65,6 +65,7 @@ export interface INotebookService { registerNotebookSerializer(viewType: string, extensionData: NotebookExtensionDescription, serializer: INotebookSerializer): IDisposable; withNotebookDataProvider(viewType: string): Promise; + tryGetDataProviderSync(viewType: string): SimpleNotebookProviderInfo | undefined; getOutputMimeTypeInfo(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined, output: IOutputDto): readonly IOrderedMimeType[]; diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellDecorations.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellDecorations.test.ts index 17fbe22abaa..7063f688505 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/cellDecorations.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/cellDecorations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts index 0db4fb19201..5867385c0aa 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts @@ -6,7 +6,7 @@ import { performCellDropEdits } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; -import * as assert from 'assert'; +import assert from 'assert'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts index 357927dbf60..65ea08b7f27 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; import { changeCellToKind, computeCellLinesContents, copyCellRange, insertCell, joinNotebookCells, moveCellRange, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellOutput.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellOutput.test.ts new file mode 100644 index 00000000000..f08ca269a47 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/browser/cellOutput.test.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { CellOutputContainer } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput'; +import { CodeCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { CellKind, INotebookRendererInfo, IOutputDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { FastDomNode } from 'vs/base/browser/fastDomNode'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { mock } from 'vs/base/test/common/mock'; +import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; +import { Event } from 'vs/base/common/event'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; + +suite('CellOutput', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; + let outputMenus: IMenu[] = []; + + setup(() => { + outputMenus = []; + instantiationService = setupInstantiationService(store); + instantiationService.stub(INotebookService, new class extends mock() { + override getOutputMimeTypeInfo() { + return [{ + rendererId: 'plainTextRendererId', + mimeType: 'text/plain', + isTrusted: true + }, { + rendererId: 'htmlRendererId', + mimeType: 'text/html', + isTrusted: true + }]; + } + override getRendererInfo(): INotebookRendererInfo { + return { + id: 'rendererId', + displayName: 'Stubbed Renderer', + extensionId: { _lower: 'id', value: 'id' }, + } as INotebookRendererInfo; + } + }); + instantiationService.stub(IMenuService, new class extends mock() { + override createMenu() { + const menu = new class extends mock() { + override onDidChange = Event.None; + override getActions() { return []; } + override dispose() { outputMenus = outputMenus.filter(item => item !== menu); } + }; + outputMenus.push(menu); + return menu; + } + }); + }); + + test('Render cell output items with multiple mime types', async function () { + const outputItem = { data: VSBuffer.fromString('output content'), mime: 'text/plain' }; + const htmlOutputItem = { data: VSBuffer.fromString('output content'), mime: 'text/html' }; + const output1: IOutputDto = { outputId: 'abc', outputs: [outputItem, htmlOutputItem] }; + const output2: IOutputDto = { outputId: 'def', outputs: [outputItem, htmlOutputItem] }; + + await withTestNotebook( + [ + ['print(output content)', 'python', CellKind.Code, [output1, output2], {}], + ], + (editor, viewModel, disposables, accessor) => { + + const cell = viewModel.viewCells[0] as CodeCellViewModel; + const cellTemplate = createCellTemplate(disposables); + const output = disposables.add(accessor.createInstance(CellOutputContainer, editor, cell, cellTemplate, { limit: 100 })); + output.render(); + cell.outputsViewModels[0].setVisible(true); + assert.strictEqual(outputMenus.length, 1, 'should have 1 output menus'); + assert(cellTemplate.outputContainer.domNode.style.display !== 'none', 'output container should be visible'); + cell.outputsViewModels[1].setVisible(true); + assert.strictEqual(outputMenus.length, 2, 'should have 2 output menus'); + cell.outputsViewModels[1].setVisible(true); + assert.strictEqual(outputMenus.length, 2, 'should still have 2 output menus'); + }, + instantiationService + ); + }); + + test('One of many cell outputs becomes hidden', async function () { + const outputItem = { data: VSBuffer.fromString('output content'), mime: 'text/plain' }; + const htmlOutputItem = { data: VSBuffer.fromString('output content'), mime: 'text/html' }; + const output1: IOutputDto = { outputId: 'abc', outputs: [outputItem, htmlOutputItem] }; + const output2: IOutputDto = { outputId: 'def', outputs: [outputItem, htmlOutputItem] }; + const output3: IOutputDto = { outputId: 'ghi', outputs: [outputItem, htmlOutputItem] }; + + await withTestNotebook( + [ + ['print(output content)', 'python', CellKind.Code, [output1, output2, output3], {}], + ], + (editor, viewModel, disposables, accessor) => { + + const cell = viewModel.viewCells[0] as CodeCellViewModel; + const cellTemplate = createCellTemplate(disposables); + const output = disposables.add(accessor.createInstance(CellOutputContainer, editor, cell, cellTemplate, { limit: 100 })); + output.render(); + cell.outputsViewModels[0].setVisible(true); + cell.outputsViewModels[1].setVisible(true); + cell.outputsViewModels[2].setVisible(true); + cell.outputsViewModels[1].setVisible(false); + assert(cellTemplate.outputContainer.domNode.style.display !== 'none', 'output container should be visible'); + assert.strictEqual(outputMenus.length, 2, 'should have 2 output menus'); + }, + instantiationService + ); + }); + + +}); + +function createCellTemplate(disposables: DisposableStore) { + return { + outputContainer: new FastDomNode(document.createElement('div')), + outputShowMoreContainer: new FastDomNode(document.createElement('div')), + focusSinkElement: document.createElement('div'), + templateDisposables: disposables, + elementDisposables: disposables, + } as unknown as CodeCellRenderTemplate; +} diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts index fefb3d36cff..13b2d31e380 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/executionStatusBarItem.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/executionStatusBarItem.test.ts index 80931b652e6..342bbd90a93 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/executionStatusBarItem.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/executionStatusBarItem.test.ts @@ -8,7 +8,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { formatCellDuration } from 'vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts index c8f3a958085..7f9571b91e1 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { FindMatch, ITextBuffer, ValidAnnotatedEditOperation } from 'vs/editor/common/model'; import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/core/wordHelper'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/layoutActions.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/layoutActions.test.ts index 886806afb68..11a24c15fec 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/layoutActions.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/layoutActions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ToggleCellToolbarPositionAction } from 'vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts index ef1c965adb2..86636c92a0b 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts index de3e3c30e92..501dcb337c9 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookClipboard.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { NotebookClipboardContribution, runCopyCells, runCutCells } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard'; import { CellKind, NOTEBOOK_EDITOR_ID, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts index 9145f0bf00b..ed6e5f6363b 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; import { IFileIconTheme, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -18,6 +18,9 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { NotebookCellOutline } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; +import { IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; suite('Notebook Outline', function () { @@ -32,6 +35,7 @@ suite('Notebook Outline', function () { disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); instantiationService.set(IEditorService, new class extends mock() { }); + instantiationService.set(ILanguageFeaturesService, new LanguageFeaturesService()); instantiationService.set(IMarkerService, disposables.add(new MarkerService())); instantiationService.set(IThemeService, new class extends mock() { override onDidFileIconThemeChange = Event.None; @@ -52,6 +56,7 @@ suite('Notebook Outline', function () { return editor; } override onDidChangeModel: Event = Event.None; + override onDidChangeSelection: Event = Event.None; }, OutlineTarget.OutlinePane); disposables.add(outline); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts index e9d26500bed..8f4eb76f909 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IDataSource } from 'vs/base/browser/ui/tree/tree'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IReference } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ITextModel } from 'vs/editor/common/model'; @@ -14,15 +15,17 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NotebookBreadcrumbsProvider, NotebookCellOutline, NotebookOutlinePaneProvider, NotebookQuickPickProvider } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookCellOutlineDataSource } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource'; import { NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; import { OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { MockDocumentSymbol } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; -import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; suite('Notebook Outline View Providers', function () { + // #region Setup - ensureNoDisposablesAreLeakedInTestSuite(); + + const store = ensureNoDisposablesAreLeakedInTestSuite(); const configurationService = new TestConfigurationService(); const themeService = new TestThemeService(); @@ -52,8 +55,10 @@ suite('Notebook Outline View Providers', function () { return 0; } }; + // #endregion // #region Helpers + function createCodeCellViewModel(version: number = 1, source = '# code', textmodelId = 'textId') { return { textBuffer: { @@ -75,6 +80,15 @@ suite('Notebook Outline View Providers', function () { } as ICellViewModel; } + function createMockOutlineDataSource(entries: OutlineEntry[], activeElement: OutlineEntry | undefined = undefined) { + return new class extends mock>() { + override object: INotebookCellOutlineDataSource = { + entries: entries, + activeElement: activeElement, + }; + }; + } + function createMarkupCellViewModel(version: number = 1, source = 'markup', textmodelId = 'textId', alternativeId = 1) { return { textBuffer: { @@ -99,7 +113,7 @@ suite('Notebook Outline View Providers', function () { } as ICellViewModel; } - function flatten(element: NotebookCellOutline | OutlineEntry, dataSource: IDataSource): OutlineEntry[] { + function flatten(element: OutlineEntry, dataSource: IDataSource): OutlineEntry[] { const elements: OutlineEntry[] = []; const children = dataSource.getChildren(element); @@ -166,6 +180,7 @@ suite('Notebook Outline View Providers', function () { await configurationService.setUserConfiguration('notebook.gotoSymbols.showAllSymbols', config.quickPickShowAllSymbols); await configurationService.setUserConfiguration('notebook.breadcrumbs.showCodeCells', config.breadcrumbsShowCodeCells); } + // #endregion // #region OutlinePane @@ -199,11 +214,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const outlinePaneProvider = new NotebookOutlinePaneProvider(() => [], configurationService); + const outlinePaneProvider = store.add(new NotebookOutlinePaneProvider(undefined, configurationService)); const results = flatten(outlineModel, outlinePaneProvider); // Validate @@ -242,11 +257,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const outlinePaneProvider = new NotebookOutlinePaneProvider(() => [], configurationService); + const outlinePaneProvider = store.add(new NotebookOutlinePaneProvider(undefined, configurationService)); const results = flatten(outlineModel, outlinePaneProvider); assert.equal(results.length, 2); @@ -288,11 +303,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const outlinePaneProvider = new NotebookOutlinePaneProvider(() => [], configurationService); + const outlinePaneProvider = store.add(new NotebookOutlinePaneProvider(undefined, configurationService)); const results = flatten(outlineModel, outlinePaneProvider); assert.equal(results.length, 1); @@ -331,11 +346,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const outlinePaneProvider = new NotebookOutlinePaneProvider(() => [], configurationService); + const outlinePaneProvider = store.add(new NotebookOutlinePaneProvider(undefined, configurationService)); const results = flatten(outlineModel, outlinePaneProvider); assert.equal(results.length, 3); @@ -380,11 +395,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const outlinePaneProvider = new NotebookOutlinePaneProvider(() => [], configurationService); + const outlinePaneProvider = store.add(new NotebookOutlinePaneProvider(undefined, configurationService)); const results = flatten(outlineModel, outlinePaneProvider); // validate @@ -439,11 +454,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const quickPickProvider = new NotebookQuickPickProvider(() => [...outlineModel.children], configurationService, themeService); + const quickPickProvider = store.add(new NotebookQuickPickProvider(createMockOutlineDataSource([...outlineModel.children]), configurationService, themeService)); const results = quickPickProvider.getQuickPickElements(); // Validate @@ -492,11 +507,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const quickPickProvider = new NotebookQuickPickProvider(() => [...outlineModel.children], configurationService, themeService); + const quickPickProvider = store.add(new NotebookQuickPickProvider(createMockOutlineDataSource([...outlineModel.children]), configurationService, themeService)); const results = quickPickProvider.getQuickPickElements(); // Validate @@ -545,11 +560,11 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createCodeCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } // Generate filtered outline (view model) - const quickPickProvider = new NotebookQuickPickProvider(() => [...outlineModel.children], configurationService, themeService); + const quickPickProvider = store.add(new NotebookQuickPickProvider(createMockOutlineDataSource([...outlineModel.children]), configurationService, themeService)); const results = quickPickProvider.getQuickPickElements(); // Validate @@ -601,12 +616,12 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createMarkupCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } const outlineTree = buildOutlineTree([...outlineModel.children]); // Generate filtered outline (view model) - const breadcrumbsProvider = new NotebookBreadcrumbsProvider(() => [...outlineTree![0].children][1], configurationService); + const breadcrumbsProvider = store.add(new NotebookBreadcrumbsProvider(createMockOutlineDataSource([], [...outlineTree![0].children][1]), configurationService)); const results = breadcrumbsProvider.getBreadcrumbElements(); // Validate @@ -652,12 +667,12 @@ suite('Notebook Outline View Providers', function () { // Generate raw outline const outlineModel = new OutlineEntry(-1, -1, createMarkupCellViewModel(), 'fakeRoot', false, false, undefined, undefined); for (const cell of cells) { - entryFactory.getOutlineEntries(cell, OutlineTarget.OutlinePane, 0).forEach(entry => outlineModel.addChild(entry)); + entryFactory.getOutlineEntries(cell, 0).forEach(entry => outlineModel.addChild(entry)); } const outlineTree = buildOutlineTree([...outlineModel.children]); // Generate filtered outline (view model) - const breadcrumbsProvider = new NotebookBreadcrumbsProvider(() => [...outlineTree![0].children][1], configurationService); + const breadcrumbsProvider = store.add(new NotebookBreadcrumbsProvider(createMockOutlineDataSource([], [...outlineTree![0].children][1]), configurationService)); const results = breadcrumbsProvider.getBreadcrumbElements(); // Validate diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts index 90d732a0cf1..dfe5b81e3f4 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -13,7 +13,6 @@ import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBr import { NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { MockDocumentSymbol } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; -import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; suite('Notebook Symbols', function () { ensureNoDisposablesAreLeakedInTestSuite(); @@ -67,7 +66,7 @@ suite('Notebook Symbols', function () { test('Cell without symbols cache', function () { setSymbolsForTextModel([{ name: 'var', range: {} }]); const entryFactory = new NotebookOutlineEntryFactory(executionService); - const entries = entryFactory.getOutlineEntries(createCellViewModel(), OutlineTarget.QuickPick, 0); + const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); assert.equal(entries.length, 1, 'no entries created'); assert.equal(entries[0].label, '# code', 'entry should fall back to first line of cell'); @@ -79,7 +78,7 @@ suite('Notebook Symbols', function () { const cell = createCellViewModel(); await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); - const entries = entryFactory.getOutlineEntries(cell, OutlineTarget.QuickPick, 0); + const entries = entryFactory.getOutlineEntries(cell, 0); assert.equal(entries.length, 3, 'wrong number of outline entries'); assert.equal(entries[0].label, '# code'); @@ -101,7 +100,7 @@ suite('Notebook Symbols', function () { const cell = createCellViewModel(); await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); - const entries = entryFactory.getOutlineEntries(createCellViewModel(), OutlineTarget.QuickPick, 0); + const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); assert.equal(entries.length, 6, 'wrong number of outline entries'); assert.equal(entries[0].label, '# code'); @@ -127,8 +126,8 @@ suite('Notebook Symbols', function () { await entryFactory.cacheSymbols(cell1, outlineModelService, CancellationToken.None); await entryFactory.cacheSymbols(cell2, outlineModelService, CancellationToken.None); - const entries1 = entryFactory.getOutlineEntries(createCellViewModel(1, '$1'), OutlineTarget.QuickPick, 0); - const entries2 = entryFactory.getOutlineEntries(createCellViewModel(1, '$2'), OutlineTarget.QuickPick, 0); + const entries1 = entryFactory.getOutlineEntries(createCellViewModel(1, '$1'), 0); + const entries2 = entryFactory.getOutlineEntries(createCellViewModel(1, '$2'), 0); assert.equal(entries1.length, 2, 'wrong number of outline entries'); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts index 98628d0c1b9..0770113f20a 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookUndoRedo.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/outputCopyTests.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/outputCopyTests.test.ts index 066d89cc736..ade4aa5abc3 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/outputCopyTests.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/outputCopyTests.test.ts @@ -7,7 +7,7 @@ import { ICellOutputViewModel, ICellViewModel } from 'vs/workbench/contrib/noteb import { mock } from 'vs/base/test/common/mock'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ILogService } from 'vs/platform/log/common/log'; -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { copyCellOutput } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookBrowser.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookBrowser.test.ts index 9b6eb24d208..dedeec1068b 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookBrowser.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookBrowser.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCellAnchor.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCellAnchor.test.ts index 782c8145df2..b2dfc2be7ff 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCellAnchor.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCellAnchor.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts index 6f9de776b53..f62c916928e 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts index d18126e162b..d0c5bc4bd91 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts index 654fe7ee807..f2bb130dba9 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; import { Mimes } from 'vs/base/common/mime'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts index 1bd5ca8f03c..8bfdf049701 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { mock } from 'vs/base/test/common/mock'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts index 071017d82d6..a7849fd7319 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -15,6 +15,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, IOutputDto, NotebookData, NotebookSetting, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -28,7 +29,10 @@ suite('NotebookFileWorkingCopyModel', function () { let disposables: DisposableStore; let instantiationService: TestInstantiationService; const configurationService = new TestConfigurationService(); - const telemetryService = new class extends mock() { }; + const telemetryService = new class extends mock() { + override publicLogError2() { } + }; + const logservice = new class extends mock() { }; teardown(() => disposables.dispose()); @@ -65,7 +69,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -88,7 +93,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); assert.strictEqual(callCount, 1); @@ -123,7 +129,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -147,6 +154,7 @@ suite('NotebookFileWorkingCopyModel', function () { ), configurationService, telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -181,7 +189,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -204,7 +213,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); assert.strictEqual(callCount, 1); @@ -239,7 +249,8 @@ suite('NotebookFileWorkingCopyModel', function () { } ), configurationService, - telemetryService + telemetryService, + logservice )); try { @@ -282,7 +293,8 @@ suite('NotebookFileWorkingCopyModel', function () { notebook, notebookService, configurationService, - telemetryService + telemetryService, + logservice )); // the save method should not be set if the serializer is not yet resolved @@ -299,11 +311,25 @@ suite('NotebookFileWorkingCopyModel', function () { function mockNotebookService(notebook: NotebookTextModel, notebookSerializer: Promise | INotebookSerializer) { return new class extends mock() { + private serializer: INotebookSerializer | undefined = undefined; override async withNotebookDataProvider(viewType: string): Promise { - const serializer = await notebookSerializer; + this.serializer = await notebookSerializer; return new SimpleNotebookProviderInfo( notebook.viewType, - serializer, + this.serializer, + { + id: new ExtensionIdentifier('test'), + location: undefined + } + ); + } + override tryGetDataProviderSync(viewType: string): SimpleNotebookProviderInfo | undefined { + if (!this.serializer) { + return undefined; + } + return new SimpleNotebookProviderInfo( + notebook.viewType, + this.serializer, { id: new ExtensionIdentifier('test'), location: undefined diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts index c4b00528450..09731c28e5d 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { AsyncIterableObject } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -11,7 +11,7 @@ import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; -import { assertThrowsAsync } from 'vs/base/test/common/utils'; +import { assertThrowsAsync, ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -40,6 +40,8 @@ suite('NotebookExecutionService', () => { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + setup(function () { disposables = new DisposableStore(); @@ -80,8 +82,8 @@ suite('NotebookExecutionService', () => { contextKeyService = instantiationService.get(IContextKeyService); }); - async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel) => void | Promise) { - return _withTestNotebook(cells, (editor, viewModel) => callback(viewModel, viewModel.notebookDocument)); + async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel, disposables: DisposableStore) => void | Promise) { + return _withTestNotebook(cells, (editor, viewModel, disposables) => callback(viewModel, viewModel.notebookDocument, disposables)); } // test('ctor', () => { @@ -94,7 +96,7 @@ suite('NotebookExecutionService', () => { test('cell is not runnable when no kernel is selected', async () => { await withTestNotebook( [], - async (viewModel, textModel) => { + async (viewModel, textModel, disposables) => { const executionService = instantiationService.createInstance(NotebookExecutionService); const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); @@ -107,9 +109,9 @@ suite('NotebookExecutionService', () => { [], async (viewModel, textModel) => { - kernelService.registerKernel(new TestNotebookKernel({ languages: ['testlang'] })); - const executionService = instantiationService.createInstance(NotebookExecutionService); - const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + disposables.add(kernelService.registerKernel(new TestNotebookKernel({ languages: ['testlang'] }))); + const executionService = disposables.add(instantiationService.createInstance(NotebookExecutionService)); + const cell = disposables.add(insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); await assertThrowsAsync(async () => await executionService.executeNotebookCells(textModel, [cell.model], contextKeyService)); }); @@ -120,13 +122,13 @@ suite('NotebookExecutionService', () => { [], async (viewModel, textModel) => { const kernel = new TestNotebookKernel({ languages: ['javascript'] }); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, textModel); - const executionService = instantiationService.createInstance(NotebookExecutionService); + const executionService = disposables.add(instantiationService.createInstance(NotebookExecutionService)); const executeSpy = sinon.spy(); kernel.executeNotebookCellsRequest = executeSpy; - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); await executionService.executeNotebookCells(viewModel.notebookDocument, [cell.model], contextKeyService); assert.strictEqual(executeSpy.calledOnce, true); }); @@ -148,12 +150,12 @@ suite('NotebookExecutionService', () => { } }; - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, textModel); - const executionService = instantiationService.createInstance(NotebookExecutionService); + const executionService = disposables.add(instantiationService.createInstance(NotebookExecutionService)); const exeStateService = instantiationService.get(INotebookExecutionStateService); - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); await executionService.executeNotebookCells(textModel, [cell.model], contextKeyService); assert.strictEqual(didExecute, true); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts index 7087aa9ec1a..8dd95c484df 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { AsyncIterableObject, DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -38,6 +39,8 @@ suite('NotebookExecutionStateService', () => { disposables.dispose(); }); + ensureNoDisposablesAreLeakedInTestSuite(); + setup(function () { disposables = new DisposableStore(); @@ -69,12 +72,12 @@ suite('NotebookExecutionStateService', () => { instantiationService.set(INotebookExecutionStateService, disposables.add(instantiationService.createInstance(NotebookExecutionStateService))); }); - async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel) => void | Promise) { - return _withTestNotebook(cells, (editor, viewModel) => callback(viewModel, viewModel.notebookDocument)); + async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel, disposables: DisposableStore) => void | Promise) { + return _withTestNotebook(cells, (editor, viewModel) => callback(viewModel, viewModel.notebookDocument, disposables)); } function testCancelOnDelete(expectedCancels: number, implementsInterrupt: boolean) { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; let cancels = 0; @@ -91,15 +94,15 @@ suite('NotebookExecutionStateService', () => { cancels += handles.length; } }; - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); // Should cancel executing and pending cells, when kernel does not implement interrupt - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - const cell2 = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - const cell3 = insertCellAtIndex(viewModel, 2, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); + const cell2 = disposables.add(insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); + const cell3 = disposables.add(insertCellAtIndex(viewModel, 2, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); insertCellAtIndex(viewModel, 3, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); // Not deleted const exe = executionStateService.createCellExecution(viewModel.uri, cell.handle); // Executing exe.confirm(); @@ -126,11 +129,11 @@ suite('NotebookExecutionStateService', () => { }); test('fires onDidChangeCellExecution when cell is completed while deleted', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); @@ -153,15 +156,15 @@ suite('NotebookExecutionStateService', () => { }); test('does not fire onDidChangeCellExecution for output updates', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); const exe = executionStateService.createCellExecution(viewModel.uri, cell.handle); let didFire = false; @@ -181,15 +184,15 @@ suite('NotebookExecutionStateService', () => { // #142466 test('getCellExecution and onDidChangeCellExecution', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); const deferred = new DeferredPromise(); disposables.add(executionStateService.onDidChangeExecution(e => { @@ -213,11 +216,11 @@ suite('NotebookExecutionStateService', () => { }); }); test('getExecution and onDidChangeExecution', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const eventRaisedWithExecution: boolean[] = []; @@ -243,11 +246,11 @@ suite('NotebookExecutionStateService', () => { }); test('getExecution and onDidChangeExecution 2', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); @@ -283,15 +286,15 @@ suite('NotebookExecutionStateService', () => { }); test('force-cancel works for Cell Execution', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); + const cell = disposables.add(insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true)); executionStateService.createCellExecution(viewModel.uri, cell.handle); const exe = executionStateService.getCellExecution(cell.uri); assert.ok(exe); @@ -302,11 +305,11 @@ suite('NotebookExecutionStateService', () => { }); }); test('force-cancel works for Notebook Execution', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const eventRaisedWithExecution: boolean[] = []; @@ -324,11 +327,11 @@ suite('NotebookExecutionStateService', () => { }); }); test('force-cancel works for Cell and Notebook Execution', async function () { - return withTestNotebook([], async viewModel => { + return withTestNotebook([], async (viewModel, _document, disposables) => { testNotebookModel = viewModel.notebookDocument; const kernel = new TestNotebookKernel(); - kernelService.registerKernel(kernel); + disposables.add(kernelService.registerKernel(kernel)); kernelService.selectKernelForNotebook(kernel, viewModel.notebookDocument); const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts index 4acfd482bf8..0d397b63bf6 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookFolding.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts index a476f5caead..a037768bbed 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { setupInstantiationService } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { Emitter, Event } from 'vs/base/common/event'; import { INotebookKernel, INotebookKernelService, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl'; @@ -66,8 +66,8 @@ suite('NotebookKernelHistoryService', () => { const u1 = URI.parse('foo:///one'); - const k1 = new TestNotebookKernel({ label: 'z', viewType: 'foo' }); - const k2 = new TestNotebookKernel({ label: 'a', viewType: 'foo' }); + const k1 = new TestNotebookKernel({ label: 'z', notebookType: 'foo' }); + const k2 = new TestNotebookKernel({ label: 'a', notebookType: 'foo' }); disposables.add(kernelService.registerKernel(k1)); disposables.add(kernelService.registerKernel(k2)); @@ -102,14 +102,14 @@ suite('NotebookKernelHistoryService', () => { const kernelHistoryService = disposables.add(instantiationService.createInstance(NotebookKernelHistoryService)); - let info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + let info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.equal(info.all.length, 0); assert.ok(!info.selected); // update priorities for u1 notebook kernelService.updateKernelNotebookAffinity(k2, u1, 2); - info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.equal(info.all.length, 0); // MRU only auto selects kernel if there is only one assert.deepStrictEqual(info.selected, undefined); @@ -119,9 +119,9 @@ suite('NotebookKernelHistoryService', () => { const u1 = URI.parse('foo:///one'); - const k1 = new TestNotebookKernel({ label: 'z', viewType: 'foo' }); - const k2 = new TestNotebookKernel({ label: 'a', viewType: 'foo' }); - const k3 = new TestNotebookKernel({ label: 'b', viewType: 'foo' }); + const k1 = new TestNotebookKernel({ label: 'z', notebookType: 'foo' }); + const k2 = new TestNotebookKernel({ label: 'a', notebookType: 'foo' }); + const k3 = new TestNotebookKernel({ label: 'b', notebookType: 'foo' }); disposables.add(kernelService.registerKernel(k1)); disposables.add(kernelService.registerKernel(k2)); @@ -158,12 +158,12 @@ suite('NotebookKernelHistoryService', () => { }); const kernelHistoryService = disposables.add(instantiationService.createInstance(NotebookKernelHistoryService)); - let info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + let info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.equal(info.all.length, 1); assert.deepStrictEqual(info.selected, undefined); kernelHistoryService.addMostRecentKernel(k3); - info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.deepStrictEqual(info.all, [k3, k2]); }); }); @@ -190,9 +190,9 @@ class TestNotebookKernel implements INotebookKernel { return AsyncIterableObject.EMPTY; } - constructor(opts?: { languages?: string[]; label?: string; viewType?: string }) { + constructor(opts?: { languages?: string[]; label?: string; notebookType?: string }) { this.supportedLanguages = opts?.languages ?? [PLAINTEXT_LANGUAGE_ID]; this.label = opts?.label ?? this.label; - this.viewType = opts?.viewType ?? this.viewType; + this.viewType = opts?.notebookType ?? this.viewType; } } diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts index 1ed657a1972..b05cd81e6a7 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { setupInstantiationService } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { Emitter, Event } from 'vs/base/common/event'; import { INotebookKernel, INotebookKernelService, VariablesResult } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl'; @@ -72,7 +72,7 @@ suite('NotebookKernelService', () => { disposables.add(kernelService.registerKernel(k2)); // equal priorities -> sort by name - let info = kernelService.getMatchingKernel({ uri: u1, viewType: 'foo' }); + let info = kernelService.getMatchingKernel({ uri: u1, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); @@ -81,18 +81,18 @@ suite('NotebookKernelService', () => { kernelService.updateKernelNotebookAffinity(k2, u2, 1); // updated - info = kernelService.getMatchingKernel({ uri: u1, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: u1, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); // NOT updated - info = kernelService.getMatchingKernel({ uri: u2, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: u2, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); // reset kernelService.updateKernelNotebookAffinity(k2, u1, undefined); - info = kernelService.getMatchingKernel({ uri: u1, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: u1, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); }); @@ -103,18 +103,18 @@ suite('NotebookKernelService', () => { const kernel = new TestNotebookKernel(); disposables.add(kernelService.registerKernel(kernel)); - let info = kernelService.getMatchingKernel({ uri: notebook, viewType: 'foo' }); + let info = kernelService.getMatchingKernel({ uri: notebook, notebookType: 'foo' }); assert.strictEqual(info.all.length, 1); assert.ok(info.all[0] === kernel); const betterKernel = new TestNotebookKernel(); disposables.add(kernelService.registerKernel(betterKernel)); - info = kernelService.getMatchingKernel({ uri: notebook, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: notebook, notebookType: 'foo' }); assert.strictEqual(info.all.length, 2); kernelService.updateKernelNotebookAffinity(betterKernel, notebook, 2); - info = kernelService.getMatchingKernel({ uri: notebook, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: notebook, notebookType: 'foo' }); assert.strictEqual(info.all.length, 2); assert.ok(info.all[0] === betterKernel); assert.ok(info.all[1] === kernel); @@ -123,8 +123,8 @@ suite('NotebookKernelService', () => { test('onDidChangeSelectedNotebooks not fired on initial notebook open #121904', function () { const uri = URI.parse('foo:///one'); - const jupyter = { uri, viewType: 'jupyter' }; - const dotnet = { uri, viewType: 'dotnet' }; + const jupyter = { uri, viewType: 'jupyter', notebookType: 'jupyter' }; + const dotnet = { uri, viewType: 'dotnet', notebookType: 'dotnet' }; const jupyterKernel = new TestNotebookKernel({ viewType: jupyter.viewType }); const dotnetKernel = new TestNotebookKernel({ viewType: dotnet.viewType }); @@ -144,8 +144,8 @@ suite('NotebookKernelService', () => { test('onDidChangeSelectedNotebooks not fired on initial notebook open #121904, p2', async function () { const uri = URI.parse('foo:///one'); - const jupyter = { uri, viewType: 'jupyter' }; - const dotnet = { uri, viewType: 'dotnet' }; + const jupyter = { uri, viewType: 'jupyter', notebookType: 'jupyter' }; + const dotnet = { uri, viewType: 'dotnet', notebookType: 'dotnet' }; const jupyterKernel = new TestNotebookKernel({ viewType: jupyter.viewType }); const dotnetKernel = new TestNotebookKernel({ viewType: dotnet.viewType }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookRendererMessagingService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookRendererMessagingService.test.ts index db9868edfa7..d40658ab353 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookRendererMessagingService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookRendererMessagingService.test.ts @@ -6,7 +6,7 @@ import { NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { stub } from 'sinon'; import { NotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/browser/services/notebookRendererMessagingServiceImpl'; -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts index 5bbbf50a502..0fd1c9a92fe 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts index 647c96305ce..77ce4841a79 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -56,7 +56,6 @@ suite('NotebookProviderInfoStore', function () { displayName: 'foo', selectors: [{ filenamePattern: '*.foo' }], priority: RegisteredEditorPriority.default, - exclusive: false, providerDisplayName: 'foo', }); const barInfo = new NotebookProviderInfo({ @@ -65,7 +64,6 @@ suite('NotebookProviderInfoStore', function () { displayName: 'bar', selectors: [{ filenamePattern: '*.bar' }], priority: RegisteredEditorPriority.default, - exclusive: false, providerDisplayName: 'bar', }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts index 0cb027d1beb..f8dae486736 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts @@ -3,13 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; import { assertSnapshot } from 'vs/base/test/common/snapshot'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; import { NotebookCellOutline } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import { INotebookEditor, INotebookEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; @@ -29,23 +32,25 @@ suite('NotebookEditorStickyScroll', () => { disposables.dispose(); }); - ensureNoDisposablesAreLeakedInTestSuite(); + const store = ensureNoDisposablesAreLeakedInTestSuite(); setup(() => { disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); + instantiationService.set(ILanguageFeaturesService, new LanguageFeaturesService()); }); function getOutline(editor: any) { if (!editor.hasModel()) { assert.ok(false, 'MUST have active text editor'); } - const outline = instantiationService.createInstance(NotebookCellOutline, new class extends mock() { + const outline = store.add(instantiationService.createInstance(NotebookCellOutline, new class extends mock() { override getControl() { return editor; } override onDidChangeModel: Event = Event.None; - }, OutlineTarget.QuickPick); + override onDidChangeSelection: Event = Event.None; + }, OutlineTarget.QuickPick)); return outline; } diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts index 4c250a43ab1..abcb1828caa 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts index fa082d78e23..ee359d70e9d 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookVariablesDataSource.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { AsyncIterableObject, AsyncIterableSource } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts index f226b1c608a..c9d92f4a71b 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; @@ -66,7 +66,7 @@ suite('NotebookViewModel', () => { test('ctor', function () { const notebook = new NotebookTextModel('notebook', URI.parse('test'), [], {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false, cellContentMetadata: {} }, undoRedoService, modelService, languageService, languageDetectionService); const model = new NotebookEditorTestModel(notebook); - const options = new NotebookOptions(mainWindow, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService), false); + const options = new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService)); const eventDispatcher = new NotebookEventDispatcher(); const viewContext = new ViewContext(options, eventDispatcher, () => ({} as IBaseCellEditorOptions)); const viewModel = new NotebookViewModel('notebook', model.notebook, viewContext, null, { isReadOnly: false }, instantiationService, bulkEditService, undoRedoService, textModelService, notebookExecutionStateService); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts index f2af8c32747..199ee022a90 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts index 34c0a8c1a01..8d530e9c21c 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookWorkbenchToolbar.test.ts @@ -5,7 +5,7 @@ import { workbenchCalculateActions, workbenchDynamicCalculateActions } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar'; import { Action, IAction, Separator } from 'vs/base/common/actions'; -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; interface IActionModel { diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index 6db24ac2b10..117ce7aaeda 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -52,7 +52,7 @@ import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/vie import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { CellKind, CellUri, ICellDto2, INotebookDiffEditorModel, INotebookEditorModel, INotebookSearchOptions, IOutputDto, IResolvedNotebookEditorModel, NotebookCellExecutionState, NotebookCellMetadata, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, ICellDto2, INotebookDiffEditorModel, INotebookEditorModel, INotebookFindOptions, IOutputDto, IResolvedNotebookEditorModel, NotebookCellExecutionState, NotebookCellMetadata, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellExecuteUpdate, ICellExecutionComplete, ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookCellExecution, INotebookExecution, INotebookExecutionStateService, INotebookFailStateChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; @@ -65,7 +65,7 @@ import { EditorFontLigatures, EditorFontVariations } from 'vs/editor/common/conf import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { mainWindow } from 'vs/base/browser/window'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; -import { INotebookCellOutlineProviderFactory, NotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory'; +import { INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSourceFactory'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; export class TestCell extends NotebookCellTextModel { @@ -178,7 +178,7 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi } } -export function setupInstantiationService(disposables: DisposableStore) { +export function setupInstantiationService(disposables: Pick) { const instantiationService = disposables.add(new TestInstantiationService()); const testThemeService = new TestThemeService(); instantiationService.stub(ILanguageService, disposables.add(new LanguageService())); @@ -199,7 +199,7 @@ export function setupInstantiationService(disposables: DisposableStore) { instantiationService.stub(IKeybindingService, new MockKeybindingService()); instantiationService.stub(INotebookCellStatusBarService, disposables.add(new NotebookCellStatusBarService())); instantiationService.stub(ICodeEditorService, disposables.add(new TestCodeEditorService(testThemeService))); - instantiationService.stub(INotebookCellOutlineProviderFactory, instantiationService.createInstance(NotebookCellOutlineProviderFactory)); + instantiationService.stub(INotebookCellOutlineDataSourceFactory, instantiationService.createInstance(NotebookCellOutlineDataSourceFactory)); instantiationService.stub(ILanguageDetectionService, new class MockLanguageDetectionService implements ILanguageDetectionService { _serviceBrand: undefined; @@ -229,8 +229,9 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic }), {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, cellContentMetadata: {}, transientOutputs: false })); const model = disposables.add(new NotebookEditorTestModel(notebook)); - const notebookOptions = disposables.add(new NotebookOptions(mainWindow, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService), false)); - const viewContext = new ViewContext(notebookOptions, disposables.add(new NotebookEventDispatcher()), () => ({} as IBaseCellEditorOptions)); + const notebookOptions = disposables.add(new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService))); + const baseCellEditorOptions = new class extends mock() { }; + const viewContext = new ViewContext(notebookOptions, disposables.add(new NotebookEventDispatcher()), () => baseCellEditorOptions); const viewModel: NotebookViewModel = disposables.add(instantiationService.createInstance(NotebookViewModel, viewType, model.notebook, viewContext, null, { isReadOnly: false })); const cellList = disposables.add(createNotebookCellList(instantiationService, disposables, viewContext)); @@ -295,6 +296,7 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic override setCellEditorSelection() { } override async revealRangeInCenterIfOutsideViewportAsync() { } override async layoutNotebookCell() { } + override async createOutput() { } override async removeInset() { } override async focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { cell.focusMode = focusItem === 'editor' ? CellFocusMode.Editor @@ -310,7 +312,7 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic override get onDidChangeSelection() { return viewModel.onDidChangeSelection as Event; } override get onDidChangeOptions() { return viewModel.onDidChangeOptions; } override get onDidChangeViewCells() { return viewModel.onDidChangeViewCells; } - override async find(query: string, options: INotebookSearchOptions): Promise { + override async find(query: string, options: INotebookFindOptions): Promise { const findMatches = viewModel.find(query, options).filter(match => match.length > 0); return findMatches; } @@ -461,15 +463,16 @@ export function createNotebookCellList(instantiationService: TestInstantiationSe getTemplateId() { return 'template'; } }; + const baseCellRenderTemplate = new class extends mock() { }; const renderer: IListRenderer = { templateId: 'template', - renderTemplate() { return {} as BaseCellRenderTemplate; }, + renderTemplate() { return baseCellRenderTemplate; }, renderElement() { }, disposeTemplate() { } }; const notebookOptions = !!viewContext ? viewContext.notebookOptions - : disposables.add(new NotebookOptions(mainWindow, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService), false)); + : disposables.add(new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService))); const cellList: NotebookCellList = disposables.add(instantiationService.createInstance( NotebookCellList, 'NotebookCellList', diff --git a/src/vs/workbench/contrib/outline/browser/outline.contribution.ts b/src/vs/workbench/contrib/outline/browser/outline.contribution.ts index 0a21d1d05bd..794c8bdfdd3 100644 --- a/src/vs/workbench/contrib/outline/browser/outline.contribution.ts +++ b/src/vs/workbench/contrib/outline/browser/outline.contribution.ts @@ -65,17 +65,17 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'default': 'alwaysExpand' }, [OutlineConfigKeys.problemsEnabled]: { - 'markdownDescription': localize('outline.showProblem', "Show errors and warnings on Outline elements. Overwritten by `#problems.visibility#` when it is off."), + 'markdownDescription': localize('outline.showProblem', "Show errors and warnings on Outline elements. Overwritten by {0} when it is off.", '`#problems.visibility#`'), 'type': 'boolean', 'default': true }, [OutlineConfigKeys.problemsColors]: { - 'markdownDescription': localize('outline.problem.colors', "Use colors for errors and warnings on Outline elements. Overwritten by `#problems.visibility#` when it is off."), + 'markdownDescription': localize('outline.problem.colors', "Use colors for errors and warnings on Outline elements. Overwritten by {0} when it is off.", '`#problems.visibility#`'), 'type': 'boolean', 'default': true }, [OutlineConfigKeys.problemsBadges]: { - 'markdownDescription': localize('outline.problems.badges', "Use badges for errors and warnings on Outline elements. Overwritten by `#problems.visibility#` when it is off."), + 'markdownDescription': localize('outline.problems.badges', "Use badges for errors and warnings on Outline elements. Overwritten by {0} when it is off.", '`#problems.visibility#`'), 'type': 'boolean', 'default': true }, diff --git a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts index ca06b37e21f..3b82ad1889e 100644 --- a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts +++ b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts @@ -11,11 +11,11 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { OUTPUT_MODE_ID, LOG_MODE_ID } from 'vs/workbench/services/output/common/output'; import { MonacoWebWorker, createWebWorker } from 'vs/editor/browser/services/webWorker'; import { ICreateData, OutputLinkComputer } from 'vs/workbench/contrib/output/common/outputLinkComputer'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -export class OutputLinkProvider { +export class OutputLinkProvider extends Disposable { private static readonly DISPOSE_WORKER_TIME = 3 * 60 * 1000; // dispose worker after 3 minutes of inactivity @@ -29,6 +29,8 @@ export class OutputLinkProvider { @ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, ) { + super(); + this.disposeWorkerScheduler = new RunOnceScheduler(() => this.disposeWorker(), OutputLinkProvider.DISPOSE_WORKER_TIME); this.registerListeners(); @@ -36,7 +38,7 @@ export class OutputLinkProvider { } private registerListeners(): void { - this.contextService.onDidChangeWorkspaceFolders(() => this.updateLinkProviderWorker()); + this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.updateLinkProviderWorker())); } private updateLinkProviderWorker(): void { diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index eb83c902631..6521eeec891 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -103,8 +103,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo this.activeOutputChannelLevelIsDefaultContext = CONTEXT_ACTIVE_OUTPUT_LEVEL_IS_DEFAULT.bindTo(contextKeyService); // Register as text model content provider for output - textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this); - instantiationService.createInstance(OutputLinkProvider); + this._register(textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this)); + this._register(instantiationService.createInstance(OutputLinkProvider)); // Create output channels for already registered channels const registry = Registry.as(Extensions.OutputChannels); diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 19e5642f145..9a23e945d85 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -62,8 +62,8 @@ export class OutputViewPane extends ViewPane { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); this.scrollLockContextKey = CONTEXT_OUTPUT_SCROLL_LOCK.bindTo(this.contextKeyService); - const editorInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); - this.editor = editorInstantiationService.createInstance(OutputEditor); + const editorInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); + this.editor = this._register(editorInstantiationService.createInstance(OutputEditor)); this._register(this.editor.onTitleAreaUpdate(() => { this.updateTitle(this.editor.getTitle()); this.updateActions(); diff --git a/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts b/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts index 2f7988f5236..722c80940c9 100644 --- a/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts +++ b/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { isMacintosh, isLinux, isWindows } from 'vs/base/common/platform'; import { OutputLinkComputer } from 'vs/workbench/contrib/output/common/outputLinkComputer'; diff --git a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts index 10577676318..81653b58f6d 100644 --- a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts +++ b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts @@ -17,7 +17,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { writeTransientState } from 'vs/workbench/contrib/codeEditor/browser/toggleWordWrap'; -import { LoaderStats, isESM } from 'vs/base/common/amd'; +import { LoaderEventType, LoaderStats, isESM } from 'vs/base/common/amd'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts b/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts index 276c5278bfe..028a744f38a 100644 --- a/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts +++ b/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts @@ -20,7 +20,6 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { StartupTimings } from 'vs/workbench/contrib/performance/browser/startupTimings'; -import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { coalesce } from 'vs/base/common/arrays'; interface ITracingData { @@ -160,6 +159,7 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo return undefined; // unexpected arguments for startup heap statistics } + const windowProcessId = await this._nativeHostService.getProcessId(); const used = (performance as unknown as { memory?: { usedJSHeapSize?: number } }).memory?.usedJSHeapSize ?? 0; // https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory let minorGCs = 0; @@ -170,7 +170,7 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo try { const traceContents: { traceEvents: ITracingData[] } = JSON.parse((await this._fileService.readFile(URI.file(this._environmentService.args['trace-startup-file']))).value.toString()); for (const event of traceContents.traceEvents) { - if (event.pid !== process.pid) { + if (event.pid !== windowProcessId) { continue; } @@ -179,11 +179,9 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo // Major/Minor GC Events case 'MinorGC': minorGCs++; + break; case 'MajorGC': majorGCs++; - if (event.args && typeof event.args.usedHeapSizeAfter === 'number' && typeof event.args.usedHeapSizeBefore === 'number') { - garbage += (event.args.usedHeapSizeBefore - event.args.usedHeapSizeAfter); - } break; // GC Events that block the main thread @@ -193,6 +191,12 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo duration += event.dur; break; } + + if (event.name === 'MajorGC' || event.name === 'MinorGC') { + if (typeof event.args?.usedHeapSizeAfter === 'number' && typeof event.args.usedHeapSizeBefore === 'number') { + garbage += (event.args.usedHeapSizeBefore - event.args.usedHeapSizeAfter); + } + } } return { minorGCs, majorGCs, used, garbage, duration: Math.round(duration / 1000) }; diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index dcf8e572093..6f3c5086186 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -60,7 +60,7 @@ import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetN import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; const $ = DOM.$; @@ -903,7 +903,7 @@ class ActionsColumnRenderer implements ITableRenderer(extensionContainer, $('a.extension-label', { tabindex: 0 })); @@ -1055,7 +1055,7 @@ class SourceColumnRenderer implements ITableRenderer { const foregroundColor = theme.getColor(foreground); diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 74caeb8c44e..b42d6a194a7 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -408,7 +408,8 @@ } .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title .setting-item-overrides a.modified-scope { - text-decoration: underline; + color: var(--vscode-textLink-foreground); + text-decoration: var(--text-link-decoration); cursor: pointer; } @@ -512,6 +513,11 @@ color: var(--vscode-textLink-foreground); } +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a, +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .edit-in-settings-button { + text-decoration: var(--text-link-decoration); +} + .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a:focus, .settings-editor > .settings-body .settings-tree-container .setting-item-contents .edit-in-settings-button:focus { outline: 1px solid -webkit-focus-ring-color; diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 66ca47c40e5..ccf4663736c 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -1246,6 +1246,7 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo ResourceContextKey.Resource.isEqualTo(this.userDataProfilesService.defaultProfile.settingsResource.toString())), ContextKeyExpr.not('isInDiffEditor')); const registerOpenUserSettingsEditorFromJsonAction = () => { + registerOpenUserSettingsEditorFromJsonActionDisposables.value = undefined; registerOpenUserSettingsEditorFromJsonActionDisposables.value = registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 1ce23e61678..dc76d156be6 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -500,6 +500,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc this._register(this.editor.getModel()!.onDidChangeContent(() => this.delayedRender())); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.source === ConfigurationTarget.DEFAULT)(() => this.delayedRender())); this._register(languageFeaturesService.codeActionProvider.register({ pattern: settingsEditorModel.uri.path }, this)); + this._register(userDataProfileService.onDidChangeCurrentProfile(() => this.delayedRender())); } private delayedRender(): void { diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 585b89b4014..4e68e88dc97 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -35,7 +35,7 @@ import { settingsEditIcon, settingsScopeDropDownIcon } from 'vs/workbench/contri import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; export class FolderSettingsActionViewItem extends BaseActionViewItem { @@ -45,7 +45,7 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { private container!: HTMLElement; private anchorElement!: HTMLElement; - private anchorElementHover!: IUpdatableHover; + private anchorElementHover!: IManagedHover; private labelElement!: HTMLElement; private detailsElement!: HTMLElement; private dropDownElement!: HTMLElement; @@ -93,7 +93,7 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { 'aria-haspopup': 'true', 'tabindex': '0' }, this.labelElement, this.detailsElement, this.dropDownElement); - this.anchorElementHover = this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.anchorElement, '')); + this.anchorElementHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.anchorElement, '')); this._register(DOM.addDisposableListener(this.anchorElement, DOM.EventType.MOUSE_DOWN, e => DOM.EventHelper.stop(e))); this._register(DOM.addDisposableListener(this.anchorElement, DOM.EventType.CLICK, e => this.onClick(e))); this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, e => this.onKeyUp(e))); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 0b0441c9356..779b9928b57 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -17,7 +17,7 @@ import { isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose, type IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/settingsEditor2'; @@ -109,7 +109,7 @@ export class SettingsEditor2 extends EditorPane { private static TOC_RESET_WIDTH: number = 200; private static EDITOR_MIN_WIDTH: number = 500; // Below NARROW_TOTAL_WIDTH, we only render the editor rather than the ToC. - private static NARROW_TOTAL_WIDTH: number = SettingsEditor2.TOC_RESET_WIDTH + SettingsEditor2.EDITOR_MIN_WIDTH; + private static NARROW_TOTAL_WIDTH: number = this.TOC_RESET_WIDTH + this.EDITOR_MIN_WIDTH; private static SUGGESTIONS: string[] = [ `@${MODIFIED_SETTING_TAG}`, @@ -219,6 +219,8 @@ export class SettingsEditor2 extends EditorPane { private installedExtensionIds: string[] = []; + private readonly inputChangeListener: MutableDisposable; + constructor( group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @@ -289,6 +291,7 @@ export class SettingsEditor2 extends EditorPane { if (ENABLE_LANGUAGE_FILTER && !SettingsEditor2.SUGGESTIONS.includes(`@${LANGUAGE_SETTING_TAG}`)) { SettingsEditor2.SUGGESTIONS.push(`@${LANGUAGE_SETTING_TAG}`); } + this.inputChangeListener = this._register(new MutableDisposable()); } override get minimumWidth(): number { return SettingsEditor2.EDITOR_MIN_WIDTH; } @@ -382,9 +385,9 @@ export class SettingsEditor2 extends EditorPane { // Don't block setInput on render (which can trigger an async search) this.onConfigUpdate(undefined, true).then(() => { - this._register(input.onWillDispose(() => { + this.inputChangeListener.value = input.onWillDispose(() => { this.searchWidget.setValue(''); - })); + }); // Init TOC selection this.updateTreeScrollSync(); @@ -791,10 +794,10 @@ export class SettingsEditor2 extends EditorPane { this.createTOC(this.tocTreeContainer); this.createSettingsTree(this.settingsTreeContainer); - this.splitView = new SplitView(this.bodyContainer, { + this.splitView = this._register(new SplitView(this.bodyContainer, { orientation: Orientation.HORIZONTAL, proportionalLayout: true - }); + })); const startingWidth = this.storageService.getNumber('settingsEditor2.splitViewWidth', StorageScope.PROFILE, SettingsEditor2.TOC_RESET_WIDTH); this.splitView.addView({ onDidChange: Event.None, @@ -911,7 +914,7 @@ export class SettingsEditor2 extends EditorPane { } private createSettingsTree(container: HTMLElement): void { - this.settingRenderers = this.instantiationService.createInstance(SettingTreeRenderers); + this.settingRenderers = this._register(this.instantiationService.createInstance(SettingTreeRenderers)); this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type, e.manualReset, e.scope))); this._register(this.settingRenderers.onDidOpenSettings(settingKey => { this.openSettingsFile({ revealSetting: { key: settingKey, edit: true } }); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index 6db2e7883d9..59b31491a53 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -9,7 +9,7 @@ import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -448,15 +448,27 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { updateDefaultOverrideIndicator(element: SettingsTreeSettingElement) { this.defaultOverrideIndicator.element.style.display = 'none'; - const sourceToDisplay = getDefaultValueSourceToDisplay(element); + let sourceToDisplay = getDefaultValueSourceToDisplay(element); if (sourceToDisplay !== undefined) { this.defaultOverrideIndicator.element.style.display = 'inline'; this.defaultOverrideIndicator.disposables.clear(); - const defaultOverrideHoverContent = localize('defaultOverriddenDetails', "Default setting value overridden by {0}", sourceToDisplay); + // Show source of default value when hovered + if (Array.isArray(sourceToDisplay) && sourceToDisplay.length === 1) { + sourceToDisplay = sourceToDisplay[0]; + } + + let defaultOverrideHoverContent; + if (!Array.isArray(sourceToDisplay)) { + defaultOverrideHoverContent = localize('defaultOverriddenDetails', "Default setting value overridden by `{0}`", sourceToDisplay); + } else { + sourceToDisplay = sourceToDisplay.map(source => `\`${source}\``); + defaultOverrideHoverContent = localize('multipledefaultOverriddenDetails', "A default values has been set by {0}", sourceToDisplay.slice(0, -1).join(', ') + ' & ' + sourceToDisplay.slice(-1)); + } + const showHover = (focus: boolean) => { return this.hoverService.showHover({ - content: defaultOverrideHoverContent, + content: new MarkdownString().appendMarkdown(defaultOverrideHoverContent), target: this.defaultOverrideIndicator.element, position: { hoverPosition: HoverPosition.BELOW, @@ -473,14 +485,22 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { } } -function getDefaultValueSourceToDisplay(element: SettingsTreeSettingElement): string | undefined { - let sourceToDisplay: string | undefined; +function getDefaultValueSourceToDisplay(element: SettingsTreeSettingElement): string | undefined | string[] { + let sourceToDisplay: string | undefined | string[]; const defaultValueSource = element.defaultValueSource; if (defaultValueSource) { - if (typeof defaultValueSource !== 'string') { - sourceToDisplay = defaultValueSource.displayName ?? defaultValueSource.id; + if (defaultValueSource instanceof Map) { + sourceToDisplay = []; + for (const [, value] of defaultValueSource) { + const newValue = typeof value !== 'string' ? value.displayName ?? value.id : value; + if (!sourceToDisplay.includes(newValue)) { + sourceToDisplay.push(newValue); + } + } } else if (typeof defaultValueSource === 'string') { sourceToDisplay = defaultValueSource; + } else { + sourceToDisplay = defaultValueSource.displayName ?? defaultValueSource.id; } } return sourceToDisplay; @@ -538,9 +558,19 @@ export function getIndicatorsLabelAriaLabel(element: SettingsTreeSettingElement, } // Add default override indicator text - const sourceToDisplay = getDefaultValueSourceToDisplay(element); + let sourceToDisplay = getDefaultValueSourceToDisplay(element); if (sourceToDisplay !== undefined) { - ariaLabelSections.push(localize('defaultOverriddenDetailsAriaLabel', "{0} overrides the default value", sourceToDisplay)); + if (Array.isArray(sourceToDisplay) && sourceToDisplay.length === 1) { + sourceToDisplay = sourceToDisplay[0]; + } + + let overriddenDetailsText; + if (!Array.isArray(sourceToDisplay)) { + overriddenDetailsText = localize('defaultOverriddenDetailsAriaLabel', "{0} overrides the default value", sourceToDisplay); + } else { + overriddenDetailsText = localize('multipleDefaultOverriddenDetailsAriaLabel', "{0} override the default value", sourceToDisplay.slice(0, -1).join(', ') + ' & ' + sourceToDisplay.slice(-1)); + } + ariaLabelSections.push(overriddenDetailsText); } // Add text about default values being overridden in other languages diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 80ce14bcaba..b404461da93 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -60,8 +60,8 @@ import { settingsMoreActionIcon } from 'vs/workbench/contrib/preferences/browser import { SettingsTarget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { ISettingOverrideClickEvent, SettingsTreeIndicatorsLabel, getIndicatorsLabelAriaLabel } from 'vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators'; import { ITOCEntry } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; -import { ISettingsEditorViewState, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement, inspectSetting, settingKeyToDisplayFormat } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; -import { ExcludeSettingWidget, IListDataItem, IObjectDataItem, IObjectEnumOption, IObjectKeySuggester, IObjectValueSuggester, ISettingListChangeEvent, IncludeSettingWidget, ListSettingWidget, ObjectSettingCheckboxWidget, ObjectSettingDropdownWidget, ObjectValue } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; +import { ISettingsEditorViewState, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement, inspectSetting, objectSettingSupportsRemoveDefaultValue, settingKeyToDisplayFormat } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; +import { ExcludeSettingWidget, IBoolObjectDataItem, IIncludeExcludeDataItem, IListDataItem, IObjectDataItem, IObjectEnumOption, IObjectKeySuggester, IObjectValueSuggester, IncludeSettingWidget, ListSettingWidget, ObjectSettingCheckboxWidget, ObjectSettingDropdownWidget, ObjectValue, SettingListEvent } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { LANGUAGE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, compareTwoNullableNumbers } from 'vs/workbench/contrib/preferences/common/preferences'; import { settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; @@ -74,14 +74,27 @@ import { IHoverService } from 'vs/platform/hover/browser/hover'; const $ = DOM.$; -function getIncludeExcludeDisplayValue(element: SettingsTreeSettingElement): IListDataItem[] { +function getIncludeExcludeDisplayValue(element: SettingsTreeSettingElement): IIncludeExcludeDataItem[] { + const elementDefaultValue: Record = typeof element.defaultValue === 'object' + ? element.defaultValue ?? {} + : {}; + const data = element.isConfigured ? - { ...element.defaultValue, ...element.scopeValue } : - element.defaultValue; + { ...elementDefaultValue, ...element.scopeValue } : + elementDefaultValue; return Object.keys(data) .filter(key => !!data[key]) .map(key => { + const defaultValue = elementDefaultValue[key]; + + // Get source if it's a default value + let source: string | undefined; + if (defaultValue === data[key] && element.setting.type === 'object' && element.defaultValueSource instanceof Map) { + const defaultSource = element.defaultValueSource.get(`${element.setting.key}.${key}`); + source = typeof defaultSource === 'string' ? defaultSource : defaultSource?.displayName; + } + const value = data[key]; const sibling = typeof value === 'boolean' ? undefined : value.when; return { @@ -90,7 +103,8 @@ function getIncludeExcludeDisplayValue(element: SettingsTreeSettingElement): ILi data: key }, sibling, - elementType: element.valueType + elementType: element.valueType, + source }; }); } @@ -135,6 +149,16 @@ function getObjectValueType(schema: IJSONSchema): ObjectValue['type'] { } } +function getObjectEntryValueDisplayValue(type: ObjectValue['type'], data: unknown, options: IObjectEnumOption[]): ObjectValue { + if (type === 'boolean') { + return { type, data: !!data }; + } else if (type === 'enum') { + return { type, data: '' + data, options }; + } else { + return { type, data: '' + data }; + } +} + function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectDataItem[] { const elementDefaultValue: Record = typeof element.defaultValue === 'object' ? element.defaultValue ?? {} @@ -162,22 +186,15 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData return Object.keys(data).map(key => { const defaultValue = elementDefaultValue[key]; - if (isDefined(objectProperties) && key in objectProperties) { - if (element.setting.allKeysAreBoolean) { - return { - key: { - type: 'string', - data: key - }, - value: { - type: 'boolean', - data: data[key] - }, - keyDescription: objectProperties[key].description, - removable: false - } as IObjectDataItem; - } + // Get source if it's a default value + let source: string | undefined; + if (defaultValue === data[key] && element.setting.type === 'object' && element.defaultValueSource instanceof Map) { + const defaultSource = element.defaultValueSource.get(`${element.setting.key}.${key}`); + source = typeof defaultSource === 'string' ? defaultSource : defaultSource?.displayName; + } + + if (isDefined(objectProperties) && key in objectProperties) { const valueEnumOptions = getEnumOptionsFromSchema(objectProperties[key]); return { key: { @@ -185,32 +202,29 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData data: key, options: wellDefinedKeyEnumOptions, }, - value: { - type: getObjectValueType(objectProperties[key]), - data: data[key], - options: valueEnumOptions, - }, + value: getObjectEntryValueDisplayValue(getObjectValueType(objectProperties[key]), data[key], valueEnumOptions), keyDescription: objectProperties[key].description, removable: isUndefinedOrNull(defaultValue), - } as IObjectDataItem; + resetable: !isUndefinedOrNull(defaultValue), + source + } satisfies IObjectDataItem; } - // The row is removable if it doesn't have a default value assigned. - // Otherwise, it is not removable, but its value can be reset to the default. - const removable = !defaultValue; + // The row is removable if it doesn't have a default value assigned or the setting supports removing the default value. + // If a default value is assigned and the user modified the default, it can be reset back to the default. + const removable = defaultValue === undefined || objectSettingSupportsRemoveDefaultValue(element.setting.key); + const resetable = !!defaultValue && defaultValue !== data[key]; const schema = patternsAndSchemas.find(({ pattern }) => pattern.test(key))?.schema; if (schema) { const valueEnumOptions = getEnumOptionsFromSchema(schema); return { key: { type: 'string', data: key }, - value: { - type: getObjectValueType(schema), - data: data[key], - options: valueEnumOptions, - }, + value: getObjectEntryValueDisplayValue(getObjectValueType(schema), data[key], valueEnumOptions), keyDescription: schema.description, removable, - } as IObjectDataItem; + resetable, + source + } satisfies IObjectDataItem; } const additionalValueEnums = getEnumOptionsFromSchema( @@ -221,17 +235,62 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData return { key: { type: 'string', data: key }, - value: { - type: typeof objectAdditionalProperties === 'object' ? getObjectValueType(objectAdditionalProperties) : 'string', - data: data[key], - options: additionalValueEnums, - }, + value: getObjectEntryValueDisplayValue( + typeof objectAdditionalProperties === 'object' ? getObjectValueType(objectAdditionalProperties) : 'string', + data[key], + additionalValueEnums, + ), keyDescription: typeof objectAdditionalProperties === 'object' ? objectAdditionalProperties.description : undefined, removable, - } as IObjectDataItem; + resetable, + source + } satisfies IObjectDataItem; }).filter(item => !isUndefinedOrNull(item.value.data)); } +function getBoolObjectDisplayValue(element: SettingsTreeSettingElement): IBoolObjectDataItem[] { + const elementDefaultValue: Record = typeof element.defaultValue === 'object' + ? element.defaultValue ?? {} + : {}; + + const elementScopeValue: Record = typeof element.scopeValue === 'object' + ? element.scopeValue ?? {} + : {}; + + const data = element.isConfigured ? + { ...elementDefaultValue, ...elementScopeValue } : + elementDefaultValue; + + const { objectProperties } = element.setting; + const displayValues: IBoolObjectDataItem[] = []; + for (const key in objectProperties) { + const defaultValue = elementDefaultValue[key]; + + // Get source if it's a default value + let source: string | undefined; + if (defaultValue === data[key] && element.setting.type === 'object' && element.defaultValueSource instanceof Map) { + const defaultSource = element.defaultValueSource.get(key); + source = typeof defaultSource === 'string' ? defaultSource : defaultSource?.displayName; + } + + displayValues.push({ + key: { + type: 'string', + data: key + }, + value: { + type: 'boolean', + data: !!data[key] + }, + keyDescription: objectProperties[key].description, + removable: false, + resetable: true, + source + }); + } + return displayValues; +} + function createArraySuggester(element: SettingsTreeSettingElement): IObjectKeySuggester { return (keys, idx) => { const enumOptions: IObjectEnumOption[] = []; @@ -629,12 +688,12 @@ interface ISettingComplexItemTemplate extends ISettingItemTemplate { } interface ISettingListItemTemplate extends ISettingItemTemplate { - listWidget: ListSettingWidget; + listWidget: ListSettingWidget; validationErrorMessageElement: HTMLElement; } interface ISettingIncludeExcludeItemTemplate extends ISettingItemTemplate { - includeExcludeWidget: ListSettingWidget; + includeExcludeWidget: ListSettingWidget; } interface ISettingObjectItemTemplate extends ISettingItemTemplate | undefined> { @@ -719,9 +778,9 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre abstract get templateId(): string; static readonly CONTROL_CLASS = 'setting-control-focus-target'; - static readonly CONTROL_SELECTOR = '.' + AbstractSettingRenderer.CONTROL_CLASS; + static readonly CONTROL_SELECTOR = '.' + this.CONTROL_CLASS; static readonly CONTENTS_CLASS = 'setting-item-contents'; - static readonly CONTENTS_SELECTOR = '.' + AbstractSettingRenderer.CONTENTS_CLASS; + static readonly CONTENTS_SELECTOR = '.' + this.CONTENTS_CLASS; static readonly ALL_ROWS_SELECTOR = '.monaco-list-row'; static readonly SETTING_KEY_ATTR = 'data-key'; @@ -805,7 +864,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const descriptionElement = DOM.append(container, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - toDispose.add(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, () => localize('modified', "The setting has been configured in the current scope."))); + toDispose.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, () => localize('modified', "The setting has been configured in the current scope."))); const valueElement = DOM.append(container, $('.setting-item-value')); const controlElement = DOM.append(valueElement, $('div.setting-item-control')); @@ -892,7 +951,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const titleTooltip = setting.key + (element.isConfigured ? ' - Modified' : ''); template.categoryElement.textContent = element.displayCategory ? (element.displayCategory + ': ') : ''; - template.elementDisposables.add(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), template.categoryElement, titleTooltip)); + template.elementDisposables.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.categoryElement, titleTooltip)); template.labelElement.text = element.displayLabel; template.labelElement.title = titleTooltip; @@ -1174,7 +1233,7 @@ class SettingArrayRenderer extends AbstractSettingRenderer implements ITreeRende return template; } - private computeNewList(template: ISettingListItemTemplate, e: ISettingListChangeEvent): string[] | undefined { + private computeNewList(template: ISettingListItemTemplate, e: SettingListEvent): string[] | undefined { if (template.context) { let newValue: string[] = []; if (Array.isArray(template.context.scopeValue)) { @@ -1183,33 +1242,28 @@ class SettingArrayRenderer extends AbstractSettingRenderer implements ITreeRende newValue = [...template.context.value]; } - if (e.sourceIndex !== undefined) { + if (e.type === 'move') { // A drag and drop occurred const sourceIndex = e.sourceIndex; - const targetIndex = e.targetIndex!; + const targetIndex = e.targetIndex; const splicedElem = newValue.splice(sourceIndex, 1)[0]; newValue.splice(targetIndex, 0, splicedElem); - } else if (e.targetIndex !== undefined) { - const itemValueData = e.item?.value.data.toString() ?? ''; - // Delete value - if (!e.item?.value.data && e.originalItem.value.data && e.targetIndex > -1) { - newValue.splice(e.targetIndex, 1); - } + } else if (e.type === 'remove' || e.type === 'reset') { + newValue.splice(e.targetIndex, 1); + } else if (e.type === 'change') { + const itemValueData = e.newItem.value.data.toString(); + // Update value - else if (e.item?.value.data && e.originalItem.value.data) { - if (e.targetIndex > -1) { - newValue[e.targetIndex] = itemValueData; - } - // For some reason, we are updating and cannot find original value - // Just append the value in this case - else { - newValue.push(itemValueData); - } + if (e.targetIndex > -1) { + newValue[e.targetIndex] = itemValueData; } - // Add value - else if (e.item?.value.data && !e.originalItem.value.data && e.targetIndex >= newValue.length) { + // For some reason, we are updating and cannot find original value + // Just append the value in this case + else { newValue.push(itemValueData); } + } else if (e.type === 'add') { + newValue.push(e.newItem.value.data.toString()); } if ( @@ -1280,80 +1334,9 @@ abstract class AbstractSettingObjectRenderer extends AbstractSettingRenderer imp } this.addSettingElementFocusHandler(template); - - common.toDispose.add(widget.onDidChangeList(e => { - this.onDidChangeObject(template, e); - })); - return template; } - protected onDidChangeObject(template: ISettingObjectItemTemplate, e: ISettingListChangeEvent): void { - const widget = (template.objectCheckboxWidget ?? template.objectDropdownWidget)!; - if (template.context) { - const defaultValue: Record = typeof template.context.defaultValue === 'object' - ? template.context.defaultValue ?? {} - : {}; - - const scopeValue: Record = typeof template.context.scopeValue === 'object' - ? template.context.scopeValue ?? {} - : {}; - - const newValue: Record = {}; - const newItems: IObjectDataItem[] = []; - - widget.items.forEach((item, idx) => { - // Item was updated - if (isDefined(e.item) && e.targetIndex === idx) { - newValue[e.item.key.data] = e.item.value.data; - newItems.push(e.item); - } - // All remaining items, but skip the one that we just updated - else if (isUndefinedOrNull(e.item) || e.item.key.data !== item.key.data) { - newValue[item.key.data] = item.value.data; - newItems.push(item); - } - }); - - // Item was deleted - if (isUndefinedOrNull(e.item)) { - delete newValue[e.originalItem.key.data]; - - const itemToDelete = newItems.findIndex(item => item.key.data === e.originalItem.key.data); - const defaultItemValue = defaultValue[e.originalItem.key.data] as string | boolean; - - // Item does not have a default - if (isUndefinedOrNull(defaultValue[e.originalItem.key.data]) && itemToDelete > -1) { - newItems.splice(itemToDelete, 1); - } else if (itemToDelete > -1) { - newItems[itemToDelete].value.data = defaultItemValue; - } - } - // New item was added - else if (widget.isItemNew(e.originalItem) && e.item.key.data !== '') { - newValue[e.item.key.data] = e.item.value.data; - newItems.push(e.item); - } - - Object.entries(newValue).forEach(([key, value]) => { - // value from the scope has changed back to the default - if (scopeValue[key] !== value && defaultValue[key] === value) { - delete newValue[key]; - } - }); - - const newObject = Object.keys(newValue).length === 0 ? undefined : newValue; - - if (template.objectCheckboxWidget) { - template.objectCheckboxWidget.setValue(newItems); - } else { - template.objectDropdownWidget!.setValue(newItems); - } - - template.onChange?.(newObject); - } - } - renderElement(element: ITreeNode, index: number, templateData: ISettingObjectItemTemplate): void { super.renderSettingElement(element, index, templateData); } @@ -1365,7 +1348,82 @@ class SettingObjectRenderer extends AbstractSettingObjectRenderer implements ITr renderTemplate(container: HTMLElement): ISettingObjectItemTemplate { const common = this.renderCommonTemplate(null, container, 'list'); const widget = this._instantiationService.createInstance(ObjectSettingDropdownWidget, common.controlElement); - return this.renderTemplateWithWidget(common, widget); + const template = this.renderTemplateWithWidget(common, widget); + common.toDispose.add(widget.onDidChangeList(e => { + this.onDidChangeObject(template, e); + })); + return template; + } + + private onDidChangeObject(template: ISettingObjectItemTemplate, e: SettingListEvent): void { + const widget = template.objectDropdownWidget!; + if (template.context) { + const settingSupportsRemoveDefault = objectSettingSupportsRemoveDefaultValue(template.context.setting.key); + const defaultValue: Record = typeof template.context.defaultValue === 'object' + ? template.context.defaultValue ?? {} + : {}; + + const scopeValue: Record = typeof template.context.scopeValue === 'object' + ? template.context.scopeValue ?? {} + : {}; + + const newValue: Record = { ...template.context.scopeValue }; // Initialize with scoped values as removed default values are not rendered + const newItems: IObjectDataItem[] = []; + + widget.items.forEach((item, idx) => { + // Item was updated + if ((e.type === 'change' || e.type === 'move') && e.targetIndex === idx) { + // If the key of the default value is changed, remove the default value + if (e.originalItem.key.data !== e.newItem.key.data && settingSupportsRemoveDefault && e.originalItem.key.data in defaultValue) { + newValue[e.originalItem.key.data] = null; + } + newValue[e.newItem.key.data] = e.newItem.value.data; + newItems.push(e.newItem); + } + // All remaining items, but skip the one that we just updated + else if ((e.type !== 'change' && e.type !== 'move') || e.newItem.key.data !== item.key.data) { + newValue[item.key.data] = item.value.data; + newItems.push(item); + } + }); + + // Item was deleted + if (e.type === 'remove' || e.type === 'reset') { + const objectKey = e.originalItem.key.data; + const removingDefaultValue = e.type === 'remove' && settingSupportsRemoveDefault && defaultValue[objectKey] === e.originalItem.value.data; + if (removingDefaultValue) { + newValue[objectKey] = null; + } else { + delete newValue[objectKey]; + } + + const itemToDelete = newItems.findIndex(item => item.key.data === objectKey); + const defaultItemValue = defaultValue[objectKey] as string | boolean; + + // Item does not have a default or default is bing removed + if (removingDefaultValue || isUndefinedOrNull(defaultValue[objectKey]) && itemToDelete > -1) { + newItems.splice(itemToDelete, 1); + } else if (!removingDefaultValue && itemToDelete > -1) { + newItems[itemToDelete].value.data = defaultItemValue; + } + } + // New item was added + else if (e.type === 'add') { + newValue[e.newItem.key.data] = e.newItem.value.data; + newItems.push(e.newItem); + } + + Object.entries(newValue).forEach(([key, value]) => { + // value from the scope has changed back to the default + if (scopeValue[key] !== value && defaultValue[key] === value && !(settingSupportsRemoveDefault && value === null)) { + delete newValue[key]; + } + }); + + const newObject = Object.keys(newValue).length === 0 ? undefined : newValue; + template.objectDropdownWidget!.setValue(newItems); + template.onChange?.(newObject); + } } protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingObjectItemTemplate, onChange: (value: Record | undefined) => void): void { @@ -1410,12 +1468,55 @@ class SettingBoolObjectRenderer extends AbstractSettingObjectRenderer implements renderTemplate(container: HTMLElement): ISettingObjectItemTemplate { const common = this.renderCommonTemplate(null, container, 'list'); const widget = this._instantiationService.createInstance(ObjectSettingCheckboxWidget, common.controlElement); - return this.renderTemplateWithWidget(common, widget); + const template = this.renderTemplateWithWidget(common, widget); + common.toDispose.add(widget.onDidChangeList(e => { + this.onDidChangeObject(template, e); + })); + return template; } - protected override onDidChangeObject(template: ISettingObjectItemTemplate, e: ISettingListChangeEvent): void { + protected onDidChangeObject(template: ISettingObjectItemTemplate, e: SettingListEvent): void { if (template.context) { - super.onDidChangeObject(template, e); + const widget = template.objectCheckboxWidget!; + const defaultValue: Record = typeof template.context.defaultValue === 'object' + ? template.context.defaultValue ?? {} + : {}; + + const scopeValue: Record = typeof template.context.scopeValue === 'object' + ? template.context.scopeValue ?? {} + : {}; + + const newValue: Record = { ...template.context.scopeValue }; // Initialize with scoped values as removed default values are not rendered + const newItems: IBoolObjectDataItem[] = []; + + if (e.type !== 'change') { + console.warn('Unexpected event type', e.type, 'for bool object setting', template.context.setting.key); + return; + } + + widget.items.forEach((item, idx) => { + // Item was updated + if (e.targetIndex === idx) { + newValue[e.newItem.key.data] = e.newItem.value.data; + newItems.push(e.newItem); + } + // All remaining items, but skip the one that we just updated + else if (e.newItem.key.data !== item.key.data) { + newValue[item.key.data] = item.value.data; + newItems.push(item); + } + }); + + Object.entries(newValue).forEach(([key, value]) => { + // value from the scope has changed back to the default + if (scopeValue[key] !== value && defaultValue[key] === value) { + delete newValue[key]; + } + }); + + const newObject = Object.keys(newValue).length === 0 ? undefined : newValue; + template.objectCheckboxWidget!.setValue(newItems); + template.onChange?.(newObject); // Focus this setting explicitly, in case we were previously // focused on another setting and clicked a checkbox/value container @@ -1425,7 +1526,7 @@ class SettingBoolObjectRenderer extends AbstractSettingObjectRenderer implements } protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingObjectItemTemplate, onChange: (value: Record | undefined) => void): void { - const items = getObjectDisplayValue(dataElement); + const items = getBoolObjectDisplayValue(dataElement); const { key } = dataElement.setting; template.objectCheckboxWidget!.setValue(items, { @@ -1462,25 +1563,27 @@ abstract class SettingIncludeExcludeRenderer extends AbstractSettingRenderer imp return template; } - private onDidChangeIncludeExclude(template: ISettingIncludeExcludeItemTemplate, e: ISettingListChangeEvent): void { + private onDidChangeIncludeExclude(template: ISettingIncludeExcludeItemTemplate, e: SettingListEvent): void { if (template.context) { const newValue = { ...template.context.scopeValue }; // first delete the existing entry, if present - if (e.originalItem.value.data.toString() in template.context.defaultValue) { - // delete a default by overriding it - newValue[e.originalItem.value.data.toString()] = false; - } else { - delete newValue[e.originalItem.value.data.toString()]; + if (e.type !== 'add') { + if (e.originalItem.value.data.toString() in template.context.defaultValue) { + // delete a default by overriding it + newValue[e.originalItem.value.data.toString()] = false; + } else { + delete newValue[e.originalItem.value.data.toString()]; + } } // then add the new or updated entry, if present - if (e.item?.value) { - if (e.item.value.data.toString() in template.context.defaultValue && !e.item.sibling) { + if (e.type === 'change' || e.type === 'add' || e.type === 'move') { + if (e.newItem.value.data.toString() in template.context.defaultValue && !e.newItem.sibling) { // add a default by deleting its override - delete newValue[e.item.value.data.toString()]; + delete newValue[e.newItem.value.data.toString()]; } else { - newValue[e.item.value.data.toString()] = e.item.sibling ? { when: e.item.sibling } : true; + newValue[e.newItem.value.data.toString()] = e.newItem.sibling ? { when: e.newItem.sibling } : true; } } @@ -1698,7 +1801,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre const enumDescriptionsAreMarkdown = dataElement.setting.enumDescriptionsAreMarkdown; const disposables = new DisposableStore(); - template.toDispose.add(disposables); + template.elementDisposables.add(disposables); let createdDefault = false; if (!settingEnum.includes(dataElement.defaultValue)) { @@ -1835,7 +1938,7 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); const descriptionElement = DOM.append(descriptionAndValueElement, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - toDispose.add(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, localize('modified', "The setting has been configured in the current scope."))); + toDispose.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, localize('modified', "The setting has been configured in the current scope."))); const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); @@ -1946,10 +2049,10 @@ export class SettingsExtensionToggleRenderer extends AbstractSettingRenderer imp } } -export class SettingTreeRenderers { +export class SettingTreeRenderers extends Disposable { readonly onDidClickOverrideElement: Event; - private readonly _onDidChangeSetting = new Emitter(); + private readonly _onDidChangeSetting = this._register(new Emitter()); readonly onDidChangeSetting: Event; readonly onDidOpenSettings: Event; @@ -1973,6 +2076,7 @@ export class SettingTreeRenderers { @IUserDataProfilesService private readonly _userDataProfilesService: IUserDataProfilesService, @IUserDataSyncEnablementService private readonly _userDataSyncEnablementService: IUserDataSyncEnablementService, ) { + super(); this.settingActions = [ new Action('settings.resetSetting', localize('resetSettingLabel', "Reset Setting"), undefined, undefined, async context => { if (context instanceof SettingsTreeSettingElement) { @@ -2078,6 +2182,20 @@ export class SettingTreeRenderers { const settingElement = this.getSettingDOMElementForDOMElement(element); return settingElement && settingElement.getAttribute(AbstractSettingRenderer.SETTING_ID_ATTR); } + + override dispose(): void { + super.dispose(); + this.settingActions.forEach(action => { + if (isDisposable(action)) { + action.dispose(); + } + }); + this.allRenderers.forEach(renderer => { + if (isDisposable(renderer)) { + renderer.dispose(); + } + }); + } } /** @@ -2134,7 +2252,7 @@ function cleanRenderedMarkdown(element: Node): void { const tagName = (child).tagName && (child).tagName.toLowerCase(); if (tagName === 'img') { - element.removeChild(child); + child.remove(); } else { cleanRenderedMarkdown(child); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index e30c3bb549a..c900ed2c45c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -17,7 +17,7 @@ import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_S import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; -import { ConfigurationScope, EditPresentationTypes, Extensions, IConfigurationRegistry, IExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationDefaultValueSource, ConfigurationScope, EditPresentationTypes, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { Registry } from 'vs/platform/registry/common/platform'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; @@ -135,7 +135,7 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { * The source of the default value to display. * This value also accounts for extension-contributed language-specific default value overrides. */ - defaultValueSource: string | IExtensionInfo | undefined; + defaultValueSource: ConfigurationDefaultValueSource | undefined; /** * Whether the setting is configured in the selected scope. @@ -351,7 +351,8 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { this.defaultValue = overrideValues.defaultValue ?? inspected.defaultValue; const registryValues = Registry.as(Extensions.Configuration).getConfigurationDefaultsOverrides(); - const overrideValueSource = registryValues.get(`[${languageSelector}]`)?.valuesSources?.get(this.setting.key); + const source = registryValues.get(`[${languageSelector}]`)?.source; + const overrideValueSource = source instanceof Map ? source.get(this.setting.key) : undefined; if (overrideValueSource) { this.defaultValueSource = overrideValueSource; } @@ -792,11 +793,25 @@ function isIncludeSetting(setting: ISetting): boolean { return setting.key === 'files.readonlyInclude'; } -function isObjectRenderableSchema({ type }: IJSONSchema): boolean { - return type === 'string' || type === 'boolean' || type === 'integer' || type === 'number'; +// The values of the following settings when a default values has been removed +export function objectSettingSupportsRemoveDefaultValue(key: string): boolean { + return key === 'workbench.editor.customLabels.patterns'; +} + +function isObjectRenderableSchema({ type }: IJSONSchema, key: string): boolean { + if (type === 'string' || type === 'boolean' || type === 'integer' || type === 'number') { + return true; + } + + if (objectSettingSupportsRemoveDefaultValue(key) && Array.isArray(type) && type.length === 2) { + return type.includes('null') && (type.includes('string') || type.includes('boolean') || type.includes('integer') || type.includes('number')); + } + + return false; } function isObjectSetting({ + key, type, objectProperties, objectPatternProperties, @@ -838,7 +853,7 @@ function isObjectSetting({ return [schema]; }).flat(); - return flatSchemas.every(isObjectRenderableSchema); + return flatSchemas.every((schema) => isObjectRenderableSchema(schema, key)); } function settingTypeEnumRenderable(_type: string | string[]) { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 4c9362f217c..6d2472b494b 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -29,6 +29,9 @@ import { settingsSelectBackground, settingsSelectBorder, settingsSelectForegroun import { defaultButtonStyles, getInputBoxStyle, getSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import { SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; const $ = DOM.$; @@ -110,21 +113,49 @@ export class ListSettingListModel { } export interface ISettingListChangeEvent { + type: 'change'; originalItem: TDataItem; - item?: TDataItem; - targetIndex?: number; - sourceIndex?: number; + newItem: TDataItem; + targetIndex: number; } +export interface ISettingListAddEvent { + type: 'add'; + newItem: TDataItem; + targetIndex: number; +} + +export interface ISettingListMoveEvent { + type: 'move'; + originalItem: TDataItem; + newItem: TDataItem; + targetIndex: number; + sourceIndex: number; +} + +export interface ISettingListRemoveEvent { + type: 'remove'; + originalItem: TDataItem; + targetIndex: number; +} + +export interface ISettingListResetEvent { + type: 'reset'; + originalItem: TDataItem; + targetIndex: number; +} + +export type SettingListEvent = ISettingListChangeEvent | ISettingListAddEvent | ISettingListMoveEvent | ISettingListRemoveEvent | ISettingListResetEvent; + export abstract class AbstractListSettingWidget extends Disposable { private listElement: HTMLElement; private rowElements: HTMLElement[] = []; - protected readonly _onDidChangeList = this._register(new Emitter>()); + protected readonly _onDidChangeList = this._register(new Emitter>()); protected readonly model = new ListSettingListModel(this.getEmptyItem()); protected readonly listDisposables = this._register(new DisposableStore()); - readonly onDidChangeList: Event> = this._onDidChangeList.event; + readonly onDidChangeList: Event> = this._onDidChangeList.event; get domNode(): HTMLElement { return this.listElement; @@ -250,11 +281,20 @@ export abstract class AbstractListSettingWidget extend protected handleItemChange(originalItem: TDataItem, changedItem: TDataItem, idx: number) { this.model.setEditKey('none'); - this._onDidChangeList.fire({ - originalItem, - item: changedItem, - targetIndex: idx, - }); + if (this.isItemNew(originalItem)) { + this._onDidChangeList.fire({ + type: 'add', + newItem: changedItem, + targetIndex: idx, + }); + } else { + this._onDidChangeList.fire({ + type: 'change', + originalItem, + newItem: changedItem, + targetIndex: idx, + }); + } this.renderList(); } @@ -396,17 +436,17 @@ export interface IListDataItem { sibling?: string; } -interface ListSettingWidgetDragDetails { +interface ListSettingWidgetDragDetails { element: HTMLElement; - item: IListDataItem; + item: TListDataItem; itemIndex: number; } -export class ListSettingWidget extends AbstractListSettingWidget { +export class ListSettingWidget extends AbstractListSettingWidget { private keyValueSuggester: IObjectKeySuggester | undefined; private showAddButton: boolean = true; - override setValue(listData: IListDataItem[], options?: IListSetValueOptions) { + override setValue(listData: TListDataItem[], options?: IListSetValueOptions) { this.keyValueSuggester = options?.keySuggester; this.showAddButton = options?.showAddButton ?? true; super.setValue(listData); @@ -421,13 +461,13 @@ export class ListSettingWidget extends AbstractListSettingWidget super(container, themeService, contextViewService); } - protected getEmptyItem(): IListDataItem { + protected getEmptyItem(): TListDataItem { return { value: { type: 'string', data: '' } - }; + } as TListDataItem; } protected override isAddButtonVisible(): boolean { @@ -438,7 +478,7 @@ export class ListSettingWidget extends AbstractListSettingWidget return ['setting-list-widget']; } - protected getActionsForItem(item: IListDataItem, idx: number): IAction[] { + protected getActionsForItem(item: TListDataItem, idx: number): IAction[] { return [ { class: ThemeIcon.asClassName(settingsEditIcon), @@ -452,20 +492,20 @@ export class ListSettingWidget extends AbstractListSettingWidget enabled: true, id: 'workbench.action.removeListItem', tooltip: this.getLocalizedStrings().deleteActionTooltip, - run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) + run: () => this._onDidChangeList.fire({ type: 'remove', originalItem: item, targetIndex: idx }) } ] as IAction[]; } - private dragDetails: ListSettingWidgetDragDetails | undefined; + private dragDetails: ListSettingWidgetDragDetails | undefined; - private getDragImage(item: IListDataItem): HTMLElement { + private getDragImage(item: TListDataItem): HTMLElement { const dragImage = $('.monaco-drag-image'); dragImage.textContent = item.value.data; return dragImage; } - protected renderItem(item: IListDataItem, idx: number): RowElementGroup { + protected renderItem(item: TListDataItem, idx: number): RowElementGroup { const rowElement = $('.setting-list-row'); const valueElement = DOM.append(rowElement, $('.setting-list-value')); const siblingElement = DOM.append(rowElement, $('.setting-list-sibling')); @@ -477,7 +517,7 @@ export class ListSettingWidget extends AbstractListSettingWidget return { rowElement, keyElement: valueElement, valueElement: siblingElement }; } - protected addDragAndDrop(rowElement: HTMLElement, item: IListDataItem, idx: number) { + protected addDragAndDrop(rowElement: HTMLElement, item: TListDataItem, idx: number) { if (this.inReadMode) { rowElement.draggable = true; rowElement.classList.add('draggable'); @@ -497,7 +537,7 @@ export class ListSettingWidget extends AbstractListSettingWidget const dragImage = this.getDragImage(item); rowElement.ownerDocument.body.appendChild(dragImage); ev.dataTransfer.setDragImage(dragImage, -10, -10); - setTimeout(() => rowElement.ownerDocument.body.removeChild(dragImage), 0); + setTimeout(() => dragImage.remove(), 0); } })); this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_OVER, (ev) => { @@ -530,9 +570,10 @@ export class ListSettingWidget extends AbstractListSettingWidget counter = 0; if (this.dragDetails.element !== rowElement) { this._onDidChangeList.fire({ + type: 'move', originalItem: this.dragDetails.item, sourceIndex: this.dragDetails.itemIndex, - item, + newItem: item, targetIndex: idx }); } @@ -548,7 +589,7 @@ export class ListSettingWidget extends AbstractListSettingWidget })); } - protected renderEdit(item: IListDataItem, idx: number): HTMLElement { + protected renderEdit(item: TListDataItem, idx: number): HTMLElement { const rowElement = $('.setting-list-edit-row'); let valueInput: InputBox | SelectBox; let currentDisplayValue: string; @@ -580,7 +621,7 @@ export class ListSettingWidget extends AbstractListSettingWidget break; } - const updatedInputBoxItem = (): IListDataItem => { + const updatedInputBoxItem = (): TListDataItem => { const inputBox = valueInput as InputBox; return { value: { @@ -588,16 +629,16 @@ export class ListSettingWidget extends AbstractListSettingWidget data: inputBox.value }, sibling: siblingInput?.value - }; + } as TListDataItem; }; - const updatedSelectBoxItem = (selectedValue: string): IListDataItem => { + const updatedSelectBoxItem = (selectedValue: string): TListDataItem => { return { value: { type: 'enum', data: selectedValue, options: currentEnumOptions ?? [] } - }; + } as TListDataItem; }; const onKeyDown = (e: StandardKeyboardEvent) => { if (e.equals(KeyCode.Enter)) { @@ -674,17 +715,17 @@ export class ListSettingWidget extends AbstractListSettingWidget return rowElement; } - override isItemNew(item: IListDataItem): boolean { + override isItemNew(item: TListDataItem): boolean { return item.value.data === ''; } - protected addTooltipsToRow(rowElementGroup: RowElementGroup, { value, sibling }: IListDataItem) { + protected addTooltipsToRow(rowElementGroup: RowElementGroup, { value, sibling }: TListDataItem) { const title = isUndefinedOrNull(sibling) ? localize('listValueHintLabel', "List item `{0}`", value.data) : localize('listSiblingHintLabel', "List item `{0}` with sibling `${1}`", value.data, sibling); const { rowElement } = rowElementGroup; - this.listDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), rowElement, title)); + this.listDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), rowElement, title)); rowElement.setAttribute('aria-label', title); } @@ -729,22 +770,28 @@ export class ListSettingWidget extends AbstractListSettingWidget } } -export class ExcludeSettingWidget extends ListSettingWidget { +export class ExcludeSettingWidget extends ListSettingWidget { protected override getContainerClasses() { return ['setting-list-include-exclude-widget']; } - protected override addDragAndDrop(rowElement: HTMLElement, item: IListDataItem, idx: number) { + protected override addDragAndDrop(rowElement: HTMLElement, item: IIncludeExcludeDataItem, idx: number) { return; } - protected override addTooltipsToRow(rowElementGroup: RowElementGroup, { value, sibling }: IListDataItem): void { - const title = isUndefinedOrNull(sibling) - ? localize('excludePatternHintLabel', "Exclude files matching `{0}`", value.data) - : localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", value.data, sibling); + protected override addTooltipsToRow(rowElementGroup: RowElementGroup, item: IIncludeExcludeDataItem): void { + let title = isUndefinedOrNull(item.sibling) + ? localize('excludePatternHintLabel', "Exclude files matching `{0}`", item.value.data) + : localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", item.value.data, item.sibling); + + if (item.source) { + title += localize('excludeIncludeSource', ". Default value provided by `{0}`", item.source); + } + + const markdownTitle = new MarkdownString().appendMarkdown(title); const { rowElement } = rowElementGroup; - this.listDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), rowElement, title)); + this.listDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), rowElement, { markdown: markdownTitle, markdownNotSupportedFallback: title })); rowElement.setAttribute('aria-label', title); } @@ -759,22 +806,28 @@ export class ExcludeSettingWidget extends ListSettingWidget { } } -export class IncludeSettingWidget extends ListSettingWidget { +export class IncludeSettingWidget extends ListSettingWidget { protected override getContainerClasses() { return ['setting-list-include-exclude-widget']; } - protected override addDragAndDrop(rowElement: HTMLElement, item: IListDataItem, idx: number) { + protected override addDragAndDrop(rowElement: HTMLElement, item: IIncludeExcludeDataItem, idx: number) { return; } - protected override addTooltipsToRow(rowElementGroup: RowElementGroup, { value, sibling }: IListDataItem): void { - const title = isUndefinedOrNull(sibling) - ? localize('includePatternHintLabel', "Include files matching `{0}`", value.data) - : localize('includeSiblingHintLabel', "Include files matching `{0}`, only when a file matching `{1}` is present", value.data, sibling); + protected override addTooltipsToRow(rowElementGroup: RowElementGroup, item: IIncludeExcludeDataItem): void { + let title = isUndefinedOrNull(item.sibling) + ? localize('includePatternHintLabel', "Include files matching `{0}`", item.value.data) + : localize('includeSiblingHintLabel', "Include files matching `{0}`, only when a file matching `{1}` is present", item.value.data, item.sibling); + + if (item.source) { + title += localize('excludeIncludeSource', ". Default value provided by `{0}`", item.source); + } + + const markdownTitle = new MarkdownString().appendMarkdown(title); const { rowElement } = rowElementGroup; - this.listDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), rowElement, title)); + this.listDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), rowElement, { markdown: markdownTitle, markdownNotSupportedFallback: title })); rowElement.setAttribute('aria-label', title); } @@ -818,7 +871,16 @@ export interface IObjectDataItem { key: ObjectKey; value: ObjectValue; keyDescription?: string; + source?: string; removable: boolean; + resetable: boolean; +} + +export interface IIncludeExcludeDataItem { + value: ObjectKey; + elementType: SettingValueType; + sibling?: string; + source?: string; } export interface IObjectValueSuggester { @@ -886,6 +948,7 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget this.editSetting(idx) }, ]; + if (item.resetable) { + actions.push({ + class: ThemeIcon.asClassName(settingsDiscardIcon), + enabled: true, + id: 'workbench.action.resetListItem', + label: '', + tooltip: this.getLocalizedStrings().resetActionTooltip, + run: () => this._onDidChangeList.fire({ type: 'reset', originalItem: item, targetIndex: idx }) + }); + } + if (item.removable) { actions.push({ class: ThemeIcon.asClassName(settingsRemoveIcon), enabled: true, id: 'workbench.action.removeListItem', - label: this.getLocalizedStrings().deleteActionTooltip, + label: '', tooltip: this.getLocalizedStrings().deleteActionTooltip, - run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) - }); - } else { - actions.push({ - class: ThemeIcon.asClassName(settingsDiscardIcon), - enabled: true, - id: 'workbench.action.resetListItem', - label: this.getLocalizedStrings().resetActionTooltip, - tooltip: this.getLocalizedStrings().resetActionTooltip, - run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) + run: () => this._onDidChangeList.fire({ type: 'remove', originalItem: item, targetIndex: idx }) }); } @@ -1181,13 +1246,21 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget { +export interface IBoolObjectDataItem { + key: IObjectStringData; + value: IObjectBoolData; + keyDescription?: string; + source?: string; + removable: false; + resetable: boolean; +} + +export class ObjectSettingCheckboxWidget extends AbstractListSettingWidget { private currentSettingKey: string = ''; constructor( @@ -1227,7 +1309,7 @@ export class ObjectSettingCheckboxWidget extends AbstractListSettingWidget, idx: number, listFocused: boolean): HTMLElement { + protected override renderDataOrEditItem(item: IListViewItem, idx: number, listFocused: boolean): HTMLElement { const rowElement = this.renderEdit(item, idx); rowElement.setAttribute('role', 'listitem'); return rowElement; } - protected renderItem(item: IObjectDataItem, idx: number): RowElementGroup { + protected renderItem(item: IBoolObjectDataItem, idx: number): RowElementGroup { // Return just the containers, since we always render in edit mode anyway const rowElement = $('.blank-row'); const keyElement = $('.blank-row-key'); return { rowElement, keyElement }; } - protected renderEdit(item: IObjectDataItem, idx: number): HTMLElement { + protected renderEdit(item: IBoolObjectDataItem, idx: number): HTMLElement { const rowElement = $('.setting-list-edit-row.setting-list-object-row.setting-item-bool'); const changedItem = { ...item }; @@ -1342,12 +1425,12 @@ export class ObjectSettingCheckboxWidget extends AbstractListSettingWidget(JSONContributionRegistry.Extensions.JSONContribution); @@ -43,7 +45,8 @@ export class PreferencesContribution implements IWorkbenchContribution { @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, - @ITextEditorService private readonly textEditorService: ITextEditorService + @ITextEditorService private readonly textEditorService: ITextEditorService, + @ILogService private readonly logService: ILogService, ) { this.settingsListener = this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(USE_SPLIT_JSON_SETTING) || e.affectsConfiguration(DEFAULT_SETTINGS_EDITOR_SETTING)) { @@ -120,26 +123,38 @@ export class PreferencesContribution implements IWorkbenchContribution { private getSchemaModel(uri: URI): ITextModel { let schema = schemaRegistry.getSchemaContributions().schemas[uri.toString()] ?? {} /* Use empty schema if not yet registered */; - const modelContent = JSON.stringify(schema); + const modelContent = this.getSchemaContent(uri, schema); const languageSelection = this.languageService.createById('jsonc'); const model = this.modelService.createModel(modelContent, languageSelection, uri); const disposables = new DisposableStore(); disposables.add(schemaRegistry.onDidChangeSchema(schemaUri => { if (schemaUri === uri.toString()) { schema = schemaRegistry.getSchemaContributions().schemas[uri.toString()]; - model.setValue(JSON.stringify(schema)); + model.setValue(this.getSchemaContent(uri, schema)); } })); disposables.add(model.onWillDispose(() => disposables.dispose())); return model; } + private getSchemaContent(uri: URI, schema: IJSONSchema): string { + const startTime = Date.now(); + const content = getCompressedContent(schema); + if (this.logService.getLevel() === LogLevel.Debug) { + const endTime = Date.now(); + const uncompressed = JSON.stringify(schema); + this.logService.debug(`${uri.path}: ${uncompressed.length} -> ${content.length} (${Math.round((uncompressed.length - content.length) / uncompressed.length * 100)}%) Took ${endTime - startTime}ms`); + } + return content; + } + dispose(): void { dispose(this.editorOpeningListener); dispose(this.settingsListener); } } + const registry = Registry.as(Extensions.Configuration); registry.registerConfiguration({ ...workbenchConfigurationNodeBase, diff --git a/src/vs/workbench/contrib/preferences/common/settingsEditorColorRegistry.ts b/src/vs/workbench/contrib/preferences/common/settingsEditorColorRegistry.ts index 29841a28248..bd5e4ef29c9 100644 --- a/src/vs/workbench/contrib/preferences/common/settingsEditorColorRegistry.ts +++ b/src/vs/workbench/contrib/preferences/common/settingsEditorColorRegistry.ts @@ -10,36 +10,36 @@ import { PANEL_BORDER } from 'vs/workbench/common/theme'; // General setting colors export const settingsHeaderForeground = registerColor('settings.headerForeground', { light: '#444444', dark: '#e7e7e7', hcDark: '#ffffff', hcLight: '#292929' }, localize('headerForeground', "The foreground color for a section header or active title.")); -export const settingsHeaderHoverForeground = registerColor('settings.settingsHeaderHoverForeground', { light: transparent(settingsHeaderForeground, 0.7), dark: transparent(settingsHeaderForeground, 0.7), hcDark: transparent(settingsHeaderForeground, 0.7), hcLight: transparent(settingsHeaderForeground, 0.7) }, localize('settingsHeaderHoverForeground', "The foreground color for a section header or hovered title.")); +export const settingsHeaderHoverForeground = registerColor('settings.settingsHeaderHoverForeground', transparent(settingsHeaderForeground, 0.7), localize('settingsHeaderHoverForeground', "The foreground color for a section header or hovered title.")); export const modifiedItemIndicator = registerColor('settings.modifiedItemIndicator', { light: new Color(new RGBA(102, 175, 224)), dark: new Color(new RGBA(12, 125, 157)), hcDark: new Color(new RGBA(0, 73, 122)), hcLight: new Color(new RGBA(102, 175, 224)), }, localize('modifiedItemForeground', "The color of the modified setting indicator.")); -export const settingsHeaderBorder = registerColor('settings.headerBorder', { dark: PANEL_BORDER, light: PANEL_BORDER, hcDark: PANEL_BORDER, hcLight: PANEL_BORDER }, localize('settingsHeaderBorder', "The color of the header container border.")); -export const settingsSashBorder = registerColor('settings.sashBorder', { dark: PANEL_BORDER, light: PANEL_BORDER, hcDark: PANEL_BORDER, hcLight: PANEL_BORDER }, localize('settingsSashBorder', "The color of the Settings editor splitview sash border.")); +export const settingsHeaderBorder = registerColor('settings.headerBorder', PANEL_BORDER, localize('settingsHeaderBorder', "The color of the header container border.")); +export const settingsSashBorder = registerColor('settings.sashBorder', PANEL_BORDER, localize('settingsSashBorder', "The color of the Settings editor splitview sash border.")); // Enum control colors -export const settingsSelectBackground = registerColor(`settings.dropdownBackground`, { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, localize('settingsDropdownBackground', "Settings editor dropdown background.")); -export const settingsSelectForeground = registerColor('settings.dropdownForeground', { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, localize('settingsDropdownForeground', "Settings editor dropdown foreground.")); -export const settingsSelectBorder = registerColor('settings.dropdownBorder', { dark: selectBorder, light: selectBorder, hcDark: selectBorder, hcLight: selectBorder }, localize('settingsDropdownBorder', "Settings editor dropdown border.")); -export const settingsSelectListBorder = registerColor('settings.dropdownListBorder', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('settingsDropdownListBorder', "Settings editor dropdown list border. This surrounds the options and separates the options from the description.")); +export const settingsSelectBackground = registerColor(`settings.dropdownBackground`, selectBackground, localize('settingsDropdownBackground', "Settings editor dropdown background.")); +export const settingsSelectForeground = registerColor('settings.dropdownForeground', selectForeground, localize('settingsDropdownForeground', "Settings editor dropdown foreground.")); +export const settingsSelectBorder = registerColor('settings.dropdownBorder', selectBorder, localize('settingsDropdownBorder', "Settings editor dropdown border.")); +export const settingsSelectListBorder = registerColor('settings.dropdownListBorder', editorWidgetBorder, localize('settingsDropdownListBorder', "Settings editor dropdown list border. This surrounds the options and separates the options from the description.")); // Bool control colors -export const settingsCheckboxBackground = registerColor('settings.checkboxBackground', { dark: checkboxBackground, light: checkboxBackground, hcDark: checkboxBackground, hcLight: checkboxBackground }, localize('settingsCheckboxBackground', "Settings editor checkbox background.")); -export const settingsCheckboxForeground = registerColor('settings.checkboxForeground', { dark: checkboxForeground, light: checkboxForeground, hcDark: checkboxForeground, hcLight: checkboxForeground }, localize('settingsCheckboxForeground', "Settings editor checkbox foreground.")); -export const settingsCheckboxBorder = registerColor('settings.checkboxBorder', { dark: checkboxBorder, light: checkboxBorder, hcDark: checkboxBorder, hcLight: checkboxBorder }, localize('settingsCheckboxBorder', "Settings editor checkbox border.")); +export const settingsCheckboxBackground = registerColor('settings.checkboxBackground', checkboxBackground, localize('settingsCheckboxBackground', "Settings editor checkbox background.")); +export const settingsCheckboxForeground = registerColor('settings.checkboxForeground', checkboxForeground, localize('settingsCheckboxForeground', "Settings editor checkbox foreground.")); +export const settingsCheckboxBorder = registerColor('settings.checkboxBorder', checkboxBorder, localize('settingsCheckboxBorder', "Settings editor checkbox border.")); // Text control colors -export const settingsTextInputBackground = registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); -export const settingsTextInputForeground = registerColor('settings.textInputForeground', { dark: inputForeground, light: inputForeground, hcDark: inputForeground, hcLight: inputForeground }, localize('textInputBoxForeground', "Settings editor text input box foreground.")); -export const settingsTextInputBorder = registerColor('settings.textInputBorder', { dark: inputBorder, light: inputBorder, hcDark: inputBorder, hcLight: inputBorder }, localize('textInputBoxBorder', "Settings editor text input box border.")); +export const settingsTextInputBackground = registerColor('settings.textInputBackground', inputBackground, localize('textInputBoxBackground', "Settings editor text input box background.")); +export const settingsTextInputForeground = registerColor('settings.textInputForeground', inputForeground, localize('textInputBoxForeground', "Settings editor text input box foreground.")); +export const settingsTextInputBorder = registerColor('settings.textInputBorder', inputBorder, localize('textInputBoxBorder', "Settings editor text input box border.")); // Number control colors -export const settingsNumberInputBackground = registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); -export const settingsNumberInputForeground = registerColor('settings.numberInputForeground', { dark: inputForeground, light: inputForeground, hcDark: inputForeground, hcLight: inputForeground }, localize('numberInputBoxForeground', "Settings editor number input box foreground.")); -export const settingsNumberInputBorder = registerColor('settings.numberInputBorder', { dark: inputBorder, light: inputBorder, hcDark: inputBorder, hcLight: inputBorder }, localize('numberInputBoxBorder', "Settings editor number input box border.")); +export const settingsNumberInputBackground = registerColor('settings.numberInputBackground', inputBackground, localize('numberInputBoxBackground', "Settings editor number input box background.")); +export const settingsNumberInputForeground = registerColor('settings.numberInputForeground', inputForeground, localize('numberInputBoxForeground', "Settings editor number input box foreground.")); +export const settingsNumberInputBorder = registerColor('settings.numberInputBorder', inputBorder, localize('numberInputBoxBorder', "Settings editor number input box border.")); export const focusedRowBackground = registerColor('settings.focusedRowBackground', { dark: transparent(listHoverBackground, .6), @@ -55,9 +55,4 @@ export const rowHoverBackground = registerColor('settings.rowHoverBackground', { hcLight: null }, localize('settings.rowHoverBackground', "The background color of a settings row when hovered.")); -export const focusedRowBorder = registerColor('settings.focusedRowBorder', { - dark: focusBorder, - light: focusBorder, - hcDark: focusBorder, - hcLight: focusBorder -}, localize('settings.focusedRowBorder', "The color of the row's top and bottom border when the row is focused.")); +export const focusedRowBorder = registerColor('settings.focusedRowBorder', focusBorder, localize('settings.focusedRowBorder', "The color of the row's top and bottom border when the row is focused.")); diff --git a/src/vs/workbench/contrib/preferences/test/browser/keybindingsEditorContribution.test.ts b/src/vs/workbench/contrib/preferences/test/browser/keybindingsEditorContribution.test.ts index e5b24e6bd3b..55c3c4dba66 100644 --- a/src/vs/workbench/contrib/preferences/test/browser/keybindingsEditorContribution.test.ts +++ b/src/vs/workbench/contrib/preferences/test/browser/keybindingsEditorContribution.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { KeybindingEditorDecorationsRenderer } from 'vs/workbench/contrib/preferences/browser/keybindingsEditorContribution'; diff --git a/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts b/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts index 6bb2d93fe7d..c465c0472e1 100644 --- a/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts +++ b/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { settingKeyToDisplayFormat, parseQuery, IParsedQuery } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; diff --git a/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts b/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts index 26d0c30d9cb..d8d614fb7d2 100644 --- a/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts +++ b/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SmartSnippetInserter } from 'vs/workbench/contrib/preferences/common/smartSnippetInserter'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 17b7b7ac490..2cb85b71dab 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -214,8 +214,8 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce private getGlobalCommandPicks(): ICommandQuickPick[] { const globalCommandPicks: ICommandQuickPick[] = []; const scopedContextKeyService = this.editorService.activeEditorPane?.scopedContextKeyService || this.editorGroupService.activeGroup.scopedContextKeyService; - const globalCommandsMenu = this.menuService.createMenu(MenuId.CommandPalette, scopedContextKeyService); - const globalCommandsMenuActions = globalCommandsMenu.getActions() + const globalCommandsMenu = this.menuService.getMenuActions(MenuId.CommandPalette, scopedContextKeyService); + const globalCommandsMenuActions = globalCommandsMenu .reduce((r, [, actions]) => [...r, ...actions], >[]) .filter(action => action instanceof MenuItemAction && action.enabled) as MenuItemAction[]; @@ -251,9 +251,6 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce }); } - // Cleanup - globalCommandsMenu.dispose(); - return globalCommandPicks; } } diff --git a/src/vs/workbench/contrib/remote/browser/media/tunnelView.css b/src/vs/workbench/contrib/remote/browser/media/tunnelView.css index 70a8ba93492..b52d115425c 100644 --- a/src/vs/workbench/contrib/remote/browser/media/tunnelView.css +++ b/src/vs/workbench/contrib/remote/browser/media/tunnelView.css @@ -44,6 +44,11 @@ margin-top: -40px; } +.ports-view .monaco-list .monaco-list-row .ports-view-actionbar-cell .ports-view-actionbar-cell-localaddress { + color: var(--vscode-textLink-foreground); + text-decoration: var(--text-link-decoration); +} + .ports-view .monaco-list .monaco-list-row .ports-view-actionbar-cell .ports-view-actionbar-cell-localaddress:hover { text-decoration: underline; } diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 13ee310b79c..98e47977753 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -22,7 +22,7 @@ import { VIEWLET_ID } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IViewDescriptor, IViewsRegistry, Extensions, ViewContainerLocation, IViewContainersRegistry, IViewDescriptorService } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -290,7 +290,7 @@ abstract class HelpItemBase implements IHelpItem { label: string; url: string; description: string; - extensionDescription: Readonly; + extensionDescription: IExtensionDescription; }[]> { return (await Promise.all(this.values.map(async (value) => { return { @@ -419,7 +419,7 @@ class IssueReporterItem extends HelpItemBase { label: string; description: string; url: string; - extensionDescription: Readonly; + extensionDescription: IExtensionDescription; }[]> { return Promise.all(this.values.map(async (value) => { return { diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 0ba86ba97b8..a1b6e3ae2be 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -750,7 +750,7 @@ export class TunnelPanel extends ViewPane { static readonly TITLE: ILocalizedString = nls.localize2('remote.tunnel', "Ports"); private panelContainer: HTMLElement | undefined; - private table!: WorkbenchTable; + private table: WorkbenchTable | undefined; private readonly tableDisposables: DisposableStore = this._register(new DisposableStore()); private tunnelTypeContext: IContextKey; private tunnelCloseableContext: IContextKey; @@ -830,7 +830,7 @@ export class TunnelPanel extends ViewPane { updateActions(); this.registerPrivacyActions(); this.createTable(); - this.table.layout(this.height, this.width); + this.table?.layout(this.height, this.width); } })); } @@ -913,7 +913,7 @@ export class TunnelPanel extends ViewPane { this.tableDisposables.add(this.table.onDidFocus(() => this.tunnelViewFocusContext.set(true))); this.tableDisposables.add(this.table.onDidBlur(() => this.tunnelViewFocusContext.set(false))); - const rerender = () => this.table.splice(0, Number.POSITIVE_INFINITY, this.viewModel.all); + const rerender = () => this.table?.splice(0, Number.POSITIVE_INFINITY, this.viewModel.all); rerender(); let lastPortCount = this.portCount; @@ -927,7 +927,7 @@ export class TunnelPanel extends ViewPane { })); this.tableDisposables.add(this.table.onMouseClick(e => { - if (this.hasOpenLinkModifier(e.browserEvent)) { + if (this.hasOpenLinkModifier(e.browserEvent) && this.table) { const selection = this.table.getSelectedElements(); if ((selection.length === 0) || ((selection.length === 1) && (selection[0] === e.element))) { @@ -959,11 +959,11 @@ export class TunnelPanel extends ViewPane { widgetContainer.classList.add('highlight'); if (!e) { // When we are in editing mode for a new forward, rather than updating an existing one we need to reveal the input box since it might be out of view. - this.table.reveal(this.table.indexOf(this.viewModel.input)); + this.table?.reveal(this.table.indexOf(this.viewModel.input)); } } else { if (e && (e.tunnel.tunnelType !== TunnelType.Add)) { - this.table.setFocus(this.lastFocus); + this.table?.setFocus(this.lastFocus); } this.focus(); } @@ -983,7 +983,7 @@ export class TunnelPanel extends ViewPane { override focus(): void { super.focus(); - this.table.domFocus(); + this.table?.domFocus(); } private onFocusChanged(event: ITableEvent) { @@ -1045,7 +1045,7 @@ export class TunnelPanel extends ViewPane { const node: TunnelItem | undefined = event.element; if (node) { - this.table.setFocus([this.table.indexOf(node)]); + this.table?.setFocus([this.table.indexOf(node)]); this.tunnelTypeContext.set(node.tunnelType); this.tunnelCloseableContext.set(!!node.closeable); this.tunnelPrivacyContext.set(node.privacy.id); @@ -1062,7 +1062,7 @@ export class TunnelPanel extends ViewPane { this.contextMenuService.showContextMenu({ menuId: MenuId.TunnelContext, menuActionOptions: { shouldForwardArgs: true }, - contextKeyService: this.table.contextKeyService, + contextKeyService: this.table?.contextKeyService, getAnchor: () => event.anchor, getActionViewItem: (action) => { const keybinding = this.keybindingService.lookupKeybinding(action.id); @@ -1073,7 +1073,7 @@ export class TunnelPanel extends ViewPane { }, onHide: (wasCancelled?: boolean) => { if (wasCancelled) { - this.table.domFocus(); + this.table?.domFocus(); } }, getActionsContext: () => node?.strip(), @@ -1093,7 +1093,7 @@ export class TunnelPanel extends ViewPane { this.height = height; this.width = width; super.layoutBody(height, width); - this.table.layout(height, width); + this.table?.layout(height, width); } } @@ -1562,24 +1562,25 @@ namespace SetTunnelProtocolAction { export const LABEL_HTTP = nls.localize('remote.tunnel.protocolHttp', "HTTP"); export const LABEL_HTTPS = nls.localize('remote.tunnel.protocolHttps', "HTTPS"); - async function handler(arg: any, protocol: TunnelProtocol, remoteExplorerService: IRemoteExplorerService) { + async function handler(arg: any, protocol: TunnelProtocol, remoteExplorerService: IRemoteExplorerService, environmentService: IWorkbenchEnvironmentService) { if (isITunnelItem(arg)) { const attributes: Partial = { protocol }; - return remoteExplorerService.tunnelModel.configPortsAttributes.addAttributes(arg.remotePort, attributes, ConfigurationTarget.USER_REMOTE); + const target = environmentService.remoteAuthority ? ConfigurationTarget.USER_REMOTE : ConfigurationTarget.USER_LOCAL; + return remoteExplorerService.tunnelModel.configPortsAttributes.addAttributes(arg.remotePort, attributes, target); } } export function handlerHttp(): ICommandHandler { return async (accessor, arg) => { - return handler(arg, TunnelProtocol.Http, accessor.get(IRemoteExplorerService)); + return handler(arg, TunnelProtocol.Http, accessor.get(IRemoteExplorerService), accessor.get(IWorkbenchEnvironmentService)); }; } export function handlerHttps(): ICommandHandler { return async (accessor, arg) => { - return handler(arg, TunnelProtocol.Https, accessor.get(IRemoteExplorerService)); + return handler(arg, TunnelProtocol.Https, accessor.get(IRemoteExplorerService), accessor.get(IWorkbenchEnvironmentService)); }; } } @@ -1817,10 +1818,5 @@ MenuRegistry.appendMenuItem(MenuId.TunnelLocalAddressInline, ({ when: isForwardedOrDetectedExpr })); -registerColor('ports.iconRunningProcessForeground', { - light: STATUS_BAR_REMOTE_ITEM_BACKGROUND, - dark: STATUS_BAR_REMOTE_ITEM_BACKGROUND, - hcDark: STATUS_BAR_REMOTE_ITEM_BACKGROUND, - hcLight: STATUS_BAR_REMOTE_ITEM_BACKGROUND -}, nls.localize('portWithRunningProcess.foreground', "The color of the icon for a port that has an associated running process.")); +registerColor('ports.iconRunningProcessForeground', STATUS_BAR_REMOTE_ITEM_BACKGROUND, nls.localize('portWithRunningProcess.foreground', "The color of the icon for a port that has an associated running process.")); diff --git a/src/vs/workbench/contrib/replNotebook/browser/interactiveEditor.css b/src/vs/workbench/contrib/replNotebook/browser/interactiveEditor.css new file mode 100644 index 00000000000..d43c6a6e98b --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/interactiveEditor.css @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.interactive-editor .input-cell-container:focus-within .input-editor-container .monaco-editor { + outline: solid 1px var(--vscode-notebook-focusedCellBorder); +} + +.interactive-editor .input-cell-container .input-editor-container .monaco-editor { + outline: solid 1px var(--vscode-notebook-inactiveFocusedCellBorder); +} + +.interactive-editor .input-cell-container .input-focus-indicator { + top: 8px; +} + +.interactive-editor .input-cell-container .monaco-editor-background, +.interactive-editor .input-cell-container .margin-view-overlays { + background-color: var(--vscode-notebook-cellEditorBackground, var(--vscode-editor-background)); +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/media/interactive.css b/src/vs/workbench/contrib/replNotebook/browser/media/interactive.css new file mode 100644 index 00000000000..f0f5cd4821e --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/media/interactive.css @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.interactive-editor .input-cell-container { + box-sizing: border-box; +} + +.interactive-editor .input-cell-container .input-focus-indicator { + position: absolute; + left: 0px; + height: 19px; +} + +.interactive-editor .input-cell-container .input-focus-indicator::before { + border-left: 3px solid transparent; + border-radius: 2px; + margin-left: 4px; + content: ""; + position: absolute; + width: 0px; + height: 100%; + z-index: 10; + left: 0px; + top: 0px; + height: 100%; +} + +.interactive-editor .input-cell-container .run-button-container { + position: absolute; +} + +.interactive-editor .input-cell-container .run-button-container .monaco-toolbar .actions-container { + justify-content: center; +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts new file mode 100644 index 00000000000..1db3282ad9e --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts @@ -0,0 +1,261 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; +import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer, IUntypedEditorInput } from 'vs/workbench/common/editor'; +// is one contrib allowed to import from another? +import { parse } from 'vs/base/common/marshalling'; +import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { CellEditType, CellKind, NotebookSetting, NotebookWorkingCopyTypeIdentifier, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorInputOptions } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { ReplEditor } from 'vs/workbench/contrib/replNotebook/browser/replEditor'; +import { ReplEditorInput } from 'vs/workbench/contrib/replNotebook/browser/replEditorInput'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyEditorHandler, IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { extname, isEqual } from 'vs/base/common/resources'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +// eslint-disable-next-line local/code-translation-remind +import { localize2 } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { Schemas } from 'vs/base/common/network'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; +import { IInteractiveHistoryService } from 'vs/workbench/contrib/interactive/browser/interactiveHistoryService'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +type SerializedNotebookEditorData = { resource: URI; preferredResource: URI; viewType: string; options?: NotebookEditorInputOptions }; +class ReplEditorSerializer implements IEditorSerializer { + canSerialize(input: EditorInput): boolean { + return input.typeId === ReplEditorInput.ID; + } + serialize(input: EditorInput): string { + assertType(input instanceof ReplEditorInput); + const data: SerializedNotebookEditorData = { + resource: input.resource, + preferredResource: input.preferredResource, + viewType: input.viewType, + options: input.options + }; + return JSON.stringify(data); + } + deserialize(instantiationService: IInstantiationService, raw: string) { + const data = parse(raw); + if (!data) { + return undefined; + } + const { resource, viewType } = data; + if (!data || !URI.isUri(resource) || typeof viewType !== 'string') { + return undefined; + } + + const input = instantiationService.createInstance(ReplEditorInput, resource); + return input; + } +} + +Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + ReplEditor, + REPL_EDITOR_ID, + 'REPL Editor' + ), + [ + new SyncDescriptor(ReplEditorInput) + ] +); + +Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer( + ReplEditorInput.ID, + ReplEditorSerializer +); + +export class ReplDocumentContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.replDocument'; + + constructor( + @INotebookService notebookService: INotebookService, + @IEditorResolverService editorResolverService: IEditorResolverService, + @IEditorService editorService: IEditorService, + @INotebookEditorModelResolverService private readonly notebookEditorModelResolverService: INotebookEditorModelResolverService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + + editorResolverService.registerEditor( + `*.replNotebook`, + { + id: 'repl', + label: 'repl Editor', + priority: RegisteredEditorPriority.option + }, + { + canSupportResource: uri => + (uri.scheme === Schemas.untitled && extname(uri) === '.replNotebook') || + (uri.scheme === Schemas.vscodeNotebookCell && extname(uri) === '.replNotebook'), + singlePerResource: true + }, + { + createUntitledEditorInput: async ({ resource, options }) => { + const scratchpad = this.configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true; + const ref = await this.notebookEditorModelResolverService.resolve({ untitledResource: resource }, 'jupyter-notebook', { scratchpad }); + + // untitled notebooks are disposed when they get saved. we should not hold a reference + // to such a disposed notebook and therefore dispose the reference as well + ref.object.notebook.onWillDispose(() => { + ref.dispose(); + }); + return { editor: this.instantiationService.createInstance(ReplEditorInput, resource!), options }; + } + } + ); + } +} + +class ReplWindowWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution, IWorkingCopyEditorHandler { + + static readonly ID = 'workbench.contrib.replWorkingCopyEditorHandler'; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, + @IExtensionService private readonly extensionService: IExtensionService, + ) { + super(); + + this._installHandler(); + } + + handles(workingCopy: IWorkingCopyIdentifier): boolean { + const viewType = this._getViewType(workingCopy); + return !!viewType && viewType === 'jupyter-notebook' && extname(workingCopy.resource) === '.replNotebook'; + + } + + isOpen(workingCopy: IWorkingCopyIdentifier, editor: EditorInput): boolean { + if (!this.handles(workingCopy)) { + return false; + } + + return editor instanceof ReplEditorInput && isEqual(workingCopy.resource, editor.resource); + } + + createEditor(workingCopy: IWorkingCopyIdentifier): EditorInput { + return this.instantiationService.createInstance(ReplEditorInput, workingCopy.resource); + } + + private async _installHandler(): Promise { + await this.extensionService.whenInstalledExtensionsRegistered(); + + this._register(this.workingCopyEditorService.registerHandler(this)); + } + + private _getViewType(workingCopy: IWorkingCopyIdentifier): string | undefined { + return NotebookWorkingCopyTypeIdentifier.parse(workingCopy.typeId); + } +} + +registerWorkbenchContribution2(ReplWindowWorkingCopyEditorHandler.ID, ReplWindowWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(ReplDocumentContribution.ID, ReplDocumentContribution, WorkbenchPhase.BlockRestore); + + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'repl.newRepl', + title: localize2('repl.editor.open', 'New REPL Editor'), + category: 'Create', + }); + } + + async run(accessor: ServicesAccessor) { + const resource = URI.from({ scheme: Schemas.untitled, path: 'repl.replNotebook' }); + const editorInput: IUntypedEditorInput = { resource, options: { override: 'repl' } }; + + const editorService = accessor.get(IEditorService); + await editorService.openEditor(editorInput, 1); + } +}); + +export async function executeReplInput(accessor: ServicesAccessor, editorControl: { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget }) { + const bulkEditService = accessor.get(IBulkEditService); + const historyService = accessor.get(IInteractiveHistoryService); + const notebookEditorService = accessor.get(INotebookEditorService); + + if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { + const notebookDocument = editorControl.notebookEditor.textModel; + const textModel = editorControl.codeEditor.getModel(); + const activeKernel = editorControl.notebookEditor.activeKernel; + const language = activeKernel?.supportedLanguages[0] ?? PLAINTEXT_LANGUAGE_ID; + + if (notebookDocument && textModel) { + const index = notebookDocument.length - 1; + const value = textModel.getValue(); + + if (isFalsyOrWhitespace(value)) { + return; + } + + historyService.addToHistory(notebookDocument.uri, value); + textModel.setValue(''); + notebookDocument.cells[index].resetTextBuffer(textModel.getTextBuffer()); + + const collapseState = editorControl.notebookEditor.notebookOptions.getDisplayOptions().interactiveWindowCollapseCodeCells === 'fromEditor' ? + { + inputCollapsed: false, + outputCollapsed: false + } : + undefined; + + await bulkEditService.apply([ + new ResourceNotebookCellEdit(notebookDocument.uri, + { + editType: CellEditType.Replace, + index: index, + count: 0, + cells: [{ + cellKind: CellKind.Code, + mime: undefined, + language, + source: value, + outputs: [], + metadata: {}, + collapseState + }] + } + ) + ]); + + // reveal the cell into view first + const range = { start: index, end: index + 1 }; + editorControl.notebookEditor.revealCellRangeInView(range); + await editorControl.notebookEditor.executeNotebookCells(editorControl.notebookEditor.getCellsInRange({ start: index, end: index + 1 })); + + // update the selection and focus in the extension host model + const editor = notebookEditorService.getNotebookEditor(editorControl.notebookEditor.getId()); + if (editor) { + editor.setSelections([range]); + editor.setFocus(range); + } + } + } +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts b/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts new file mode 100644 index 00000000000..53a0b25667d --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts @@ -0,0 +1,726 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/interactive'; +// eslint-disable-next-line local/code-translation-remind +import * as nls from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { ICodeEditorViewState, IDecorationOptions } from 'vs/editor/common/editorCommon'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { EditorPaneSelectionChangeReason, IEditorMemento, IEditorOpenContext, IEditorPaneScrollPosition, IEditorPaneSelectionChangeEvent, IEditorPaneWithScrolling } from 'vs/workbench/common/editor'; +import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { ICellViewModel, INotebookEditorOptions, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ExecutionStateCellStatusBarContrib, TimerCellStatusBarContrib } from 'vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { InteractiveWindowSetting, INTERACTIVE_INPUT_CURSOR_BOUNDARY } from 'vs/workbench/contrib/interactive/browser/interactiveCommon'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IAction } from 'vs/base/common/actions'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { ParameterHintsController } from 'vs/editor/contrib/parameterHints/browser/parameterHints'; +import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; +import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion'; +import { MarkerController } from 'vs/editor/contrib/gotoError/browser/gotoError'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { ITextEditorOptions, TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; +import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { NOTEBOOK_KERNEL } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { isEqual } from 'vs/base/common/resources'; +import { NotebookFindContrib } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { EXECUTE_REPL_COMMAND_ID, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import 'vs/css!./interactiveEditor'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { deepClone } from 'vs/base/common/objects'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ReplEditorInput } from 'vs/workbench/contrib/replNotebook/browser/replEditorInput'; + +const DECORATION_KEY = 'interactiveInputDecoration'; +const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; + +const INPUT_CELL_VERTICAL_PADDING = 8; +const INPUT_CELL_HORIZONTAL_PADDING_RIGHT = 10; +const INPUT_EDITOR_PADDING = 8; + +export interface InteractiveEditorViewState { + readonly notebook?: INotebookEditorViewState; + readonly input?: ICodeEditorViewState | null; +} + +export interface InteractiveEditorOptions extends ITextEditorOptions { + readonly viewState?: InteractiveEditorViewState; +} + +export class ReplEditor extends EditorPane implements IEditorPaneWithScrolling { + private _rootElement!: HTMLElement; + private _styleElement!: HTMLStyleElement; + private _notebookEditorContainer!: HTMLElement; + private _notebookWidget: IBorrowValue = { value: undefined }; + private _inputCellContainer!: HTMLElement; + private _inputFocusIndicator!: HTMLElement; + private _inputRunButtonContainer!: HTMLElement; + private _inputEditorContainer!: HTMLElement; + private _codeEditorWidget!: CodeEditorWidget; + private _notebookWidgetService: INotebookEditorService; + private _instantiationService: IInstantiationService; + private _languageService: ILanguageService; + private _contextKeyService: IContextKeyService; + private _configurationService: IConfigurationService; + private _notebookKernelService: INotebookKernelService; + private _keybindingService: IKeybindingService; + private _menuService: IMenuService; + private _contextMenuService: IContextMenuService; + private _editorGroupService: IEditorGroupsService; + private _extensionService: IExtensionService; + private readonly _widgetDisposableStore: DisposableStore = this._register(new DisposableStore()); + private _lastLayoutDimensions?: { readonly dimension: DOM.Dimension; readonly position: DOM.IDomPosition }; + private _editorOptions: IEditorOptions; + private _notebookOptions: NotebookOptions; + private _editorMemento: IEditorMemento; + private readonly _groupListener = this._register(new MutableDisposable()); + private _runbuttonToolbar: ToolBar | undefined; + + private _onDidFocusWidget = this._register(new Emitter()); + override get onDidFocus(): Event { return this._onDidFocusWidget.event; } + private _onDidChangeSelection = this._register(new Emitter()); + readonly onDidChangeSelection = this._onDidChangeSelection.event; + private _onDidChangeScroll = this._register(new Emitter()); + readonly onDidChangeScroll = this._onDidChangeScroll.event; + + constructor( + group: IEditorGroup, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IInstantiationService instantiationService: IInstantiationService, + @INotebookEditorService notebookWidgetService: INotebookEditorService, + @IContextKeyService contextKeyService: IContextKeyService, + @ICodeEditorService codeEditorService: ICodeEditorService, + @INotebookKernelService notebookKernelService: INotebookKernelService, + @ILanguageService languageService: ILanguageService, + @IKeybindingService keybindingService: IKeybindingService, + @IConfigurationService configurationService: IConfigurationService, + @IMenuService menuService: IMenuService, + @IContextMenuService contextMenuService: IContextMenuService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, + @IExtensionService extensionService: IExtensionService, + ) { + super( + REPL_EDITOR_ID, + group, + telemetryService, + themeService, + storageService + ); + this._instantiationService = instantiationService; + this._notebookWidgetService = notebookWidgetService; + this._contextKeyService = contextKeyService; + this._configurationService = configurationService; + this._notebookKernelService = notebookKernelService; + this._languageService = languageService; + this._keybindingService = keybindingService; + this._menuService = menuService; + this._contextMenuService = contextMenuService; + this._editorGroupService = editorGroupService; + this._extensionService = extensionService; + + this._editorOptions = this._computeEditorOptions(); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor') || e.affectsConfiguration('notebook')) { + this._editorOptions = this._computeEditorOptions(); + } + })); + this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); + this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); + + codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); + this._register(this._keybindingService.onDidUpdateKeybindings(this._updateInputDecoration, this)); + this._register(notebookExecutionStateService.onDidChangeExecution((e) => { + if (e.type === NotebookExecutionType.cell && isEqual(e.notebook, this._notebookWidget.value?.viewModel?.notebookDocument.uri)) { + const cell = this._notebookWidget.value?.getCellByHandle(e.cellHandle); + if (cell && e.changed?.state) { + this._scrollIfNecessary(cell); + } + } + })); + } + + private get inputCellContainerHeight() { + return 19 + 2 + INPUT_CELL_VERTICAL_PADDING * 2 + INPUT_EDITOR_PADDING * 2; + } + + private get inputCellEditorHeight() { + return 19 + INPUT_EDITOR_PADDING * 2; + } + + protected createEditor(parent: HTMLElement): void { + this._rootElement = DOM.append(parent, DOM.$('.interactive-editor')); + this._rootElement.style.position = 'relative'; + this._notebookEditorContainer = DOM.append(this._rootElement, DOM.$('.notebook-editor-container')); + this._inputCellContainer = DOM.append(this._rootElement, DOM.$('.input-cell-container')); + this._inputCellContainer.style.position = 'absolute'; + this._inputCellContainer.style.height = `${this.inputCellContainerHeight}px`; + this._inputFocusIndicator = DOM.append(this._inputCellContainer, DOM.$('.input-focus-indicator')); + this._inputRunButtonContainer = DOM.append(this._inputCellContainer, DOM.$('.run-button-container')); + this._setupRunButtonToolbar(this._inputRunButtonContainer); + this._inputEditorContainer = DOM.append(this._inputCellContainer, DOM.$('.input-editor-container')); + this._createLayoutStyles(); + } + + private _setupRunButtonToolbar(runButtonContainer: HTMLElement) { + const menu = this._register(this._menuService.createMenu(MenuId.ReplInputExecute, this._contextKeyService)); + this._runbuttonToolbar = this._register(new ToolBar(runButtonContainer, this._contextMenuService, { + getKeyBinding: action => this._keybindingService.lookupKeybinding(action.id), + actionViewItemProvider: (action, options) => { + return createActionViewItem(this._instantiationService, action, options); + }, + renderDropdownAsChildElement: true + })); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result); + this._runbuttonToolbar.setActions([...primary, ...secondary]); + } + + private _createLayoutStyles(): void { + this._styleElement = DOM.createStyleSheet(this._rootElement); + const styleSheets: string[] = []; + + const { + codeCellLeftMargin, + cellRunGutter + } = this._notebookOptions.getLayoutConfiguration(); + const { + focusIndicator + } = this._notebookOptions.getDisplayOptions(); + const leftMargin = this._notebookOptions.getCellEditorContainerLeftMargin(); + + styleSheets.push(` + .interactive-editor .input-cell-container { + padding: ${INPUT_CELL_VERTICAL_PADDING}px ${INPUT_CELL_HORIZONTAL_PADDING_RIGHT}px ${INPUT_CELL_VERTICAL_PADDING}px ${leftMargin}px; + } + `); + if (focusIndicator === 'gutter') { + styleSheets.push(` + .interactive-editor .input-cell-container:focus-within .input-focus-indicator::before { + border-color: var(--vscode-notebook-focusedCellBorder) !important; + } + .interactive-editor .input-focus-indicator::before { + border-color: var(--vscode-notebook-inactiveFocusedCellBorder) !important; + } + .interactive-editor .input-cell-container .input-focus-indicator { + display: block; + top: ${INPUT_CELL_VERTICAL_PADDING}px; + } + .interactive-editor .input-cell-container { + border-top: 1px solid var(--vscode-notebook-inactiveFocusedCellBorder); + } + `); + } else { + // border + styleSheets.push(` + .interactive-editor .input-cell-container { + border-top: 1px solid var(--vscode-notebook-inactiveFocusedCellBorder); + } + .interactive-editor .input-cell-container .input-focus-indicator { + display: none; + } + `); + } + + styleSheets.push(` + .interactive-editor .input-cell-container .run-button-container { + width: ${cellRunGutter}px; + left: ${codeCellLeftMargin}px; + margin-top: ${INPUT_EDITOR_PADDING - 2}px; + } + `); + + this._styleElement.textContent = styleSheets.join('\n'); + } + + private _computeEditorOptions(): IEditorOptions { + let overrideIdentifier: string | undefined = undefined; + if (this._codeEditorWidget) { + overrideIdentifier = this._codeEditorWidget.getModel()?.getLanguageId(); + } + const editorOptions = deepClone(this._configurationService.getValue('editor', { overrideIdentifier })); + const editorOptionsOverride = getSimpleEditorOptions(this._configurationService); + const computed = Object.freeze({ + ...editorOptions, + ...editorOptionsOverride, + ...{ + glyphMargin: true, + padding: { + top: INPUT_EDITOR_PADDING, + bottom: INPUT_EDITOR_PADDING + }, + hover: { + enabled: true + } + } + }); + + return computed; + } + + protected override saveState(): void { + this._saveEditorViewState(this.input); + super.saveState(); + } + + override getViewState(): InteractiveEditorViewState | undefined { + const input = this.input; + if (!(input instanceof ReplEditorInput)) { + return undefined; + } + + this._saveEditorViewState(input); + return this._loadNotebookEditorViewState(input); + } + + private _saveEditorViewState(input: EditorInput | undefined): void { + if (this._notebookWidget.value && input instanceof ReplEditorInput) { + if (this._notebookWidget.value.isDisposed) { + return; + } + + const state = this._notebookWidget.value.getEditorViewState(); + const editorState = this._codeEditorWidget.saveViewState(); + this._editorMemento.saveEditorState(this.group, input.resource, { + notebook: state, + input: editorState + }); + } + } + + private _loadNotebookEditorViewState(input: ReplEditorInput): InteractiveEditorViewState | undefined { + const result = this._editorMemento.loadEditorState(this.group, input.resource); + if (result) { + return result; + } + // when we don't have a view state for the group/input-tuple then we try to use an existing + // editor for the same resource. + for (const group of this._editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { + if (group.activeEditorPane !== this && group.activeEditorPane === this && group.activeEditor?.matches(input)) { + const notebook = this._notebookWidget.value?.getEditorViewState(); + const input = this._codeEditorWidget.saveViewState(); + return { + notebook, + input + }; + } + } + return; + } + + override async setInput(input: ReplEditorInput, options: InteractiveEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + // there currently is a widget which we still own so + // we need to hide it before getting a new widget + this._notebookWidget.value?.onWillHide(); + + this._codeEditorWidget?.dispose(); + + this._widgetDisposableStore.clear(); + + this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group, input, { + isEmbedded: true, + isReadOnly: true, + forRepl: true, + contributions: NotebookEditorExtensionsRegistry.getSomeEditorContributions([ + ExecutionStateCellStatusBarContrib.id, + TimerCellStatusBarContrib.id, + NotebookFindContrib.id + ]), + menuIds: { + notebookToolbar: MenuId.InteractiveToolbar, + cellTitleToolbar: MenuId.InteractiveCellTitle, + cellDeleteToolbar: MenuId.InteractiveCellDelete, + cellInsertToolbar: MenuId.NotebookCellBetween, + cellTopInsertToolbar: MenuId.NotebookCellListTop, + cellExecuteToolbar: MenuId.InteractiveCellExecute, + cellExecutePrimary: undefined + }, + cellEditorContributions: EditorExtensionsRegistry.getSomeEditorContributions([ + SelectionClipboardContributionID, + ContextMenuController.ID, + HoverController.ID, + MarkerController.ID + ]), + options: this._notebookOptions, + codeWindow: this.window + }, undefined, this.window); + + this._codeEditorWidget = this._instantiationService.createInstance(CodeEditorWidget, this._inputEditorContainer, this._editorOptions, { + ...{ + isSimpleWidget: false, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + MenuPreventer.ID, + SelectionClipboardContributionID, + ContextMenuController.ID, + SuggestController.ID, + ParameterHintsController.ID, + SnippetController2.ID, + TabCompletionController.ID, + HoverController.ID, + MarkerController.ID + ]) + } + }); + + if (this._lastLayoutDimensions) { + this._notebookEditorContainer.style.height = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._notebookWidget.value!.layout(new DOM.Dimension(this._lastLayoutDimensions.dimension.width, this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight), this._notebookEditorContainer); + const leftMargin = this._notebookOptions.getCellEditorContainerLeftMargin(); + const maxHeight = Math.min(this._lastLayoutDimensions.dimension.height / 2, this.inputCellEditorHeight); + this._codeEditorWidget.layout(this._validateDimension(this._lastLayoutDimensions.dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight)); + this._inputFocusIndicator.style.height = `${this.inputCellEditorHeight}px`; + this._inputCellContainer.style.top = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._inputCellContainer.style.width = `${this._lastLayoutDimensions.dimension.width}px`; + } + + await super.setInput(input, options, context, token); + const model = await input.resolve(); + if (this._runbuttonToolbar) { + this._runbuttonToolbar.context = input.resource; + } + + if (model === null) { + throw new Error('The REPL model could not be resolved'); + } + + this._notebookWidget.value?.setParentContextKeyService(this._contextKeyService); + + const viewState = options?.viewState ?? this._loadNotebookEditorViewState(input); + await this._extensionService.whenInstalledExtensionsRegistered(); + await this._notebookWidget.value!.setModel(model.notebook, viewState?.notebook); + model.notebook.setCellCollapseDefault(this._notebookOptions.getCellCollapseDefault()); + this._notebookWidget.value!.setOptions({ + isReadOnly: true + }); + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidResizeOutput((cvm) => { + this._scrollIfNecessary(cvm); + })); + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidFocusWidget(() => this._onDidFocusWidget.fire())); + this._widgetDisposableStore.add(this._notebookOptions.onDidChangeOptions(e => { + if (e.compactView || e.focusIndicator) { + // update the styling + this._styleElement?.remove(); + this._createLayoutStyles(); + } + + if (this._lastLayoutDimensions && this.isVisible()) { + this.layout(this._lastLayoutDimensions.dimension, this._lastLayoutDimensions.position); + } + + if (e.interactiveWindowCollapseCodeCells) { + model.notebook.setCellCollapseDefault(this._notebookOptions.getCellCollapseDefault()); + } + })); + + const editorModel = await input.resolveInput(model.notebook); + this._codeEditorWidget.setModel(editorModel); + if (viewState?.input) { + this._codeEditorWidget.restoreViewState(viewState.input); + } + this._editorOptions = this._computeEditorOptions(); + this._codeEditorWidget.updateOptions(this._editorOptions); + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidFocusEditorWidget(() => this._onDidFocusWidget.fire())); + this._widgetDisposableStore.add(this._codeEditorWidget.onDidContentSizeChange(e => { + if (!e.contentHeightChanged) { + return; + } + + if (this._lastLayoutDimensions) { + this._layoutWidgets(this._lastLayoutDimensions.dimension, this._lastLayoutDimensions.position); + } + })); + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeCursorPosition(e => this._onDidChangeSelection.fire({ reason: this._toEditorPaneSelectionChangeReason(e) }))); + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModelContent(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT }))); + + + this._widgetDisposableStore.add(this._notebookKernelService.onDidChangeNotebookAffinity(this._syncWithKernel, this)); + this._widgetDisposableStore.add(this._notebookKernelService.onDidChangeSelectedNotebooks(this._syncWithKernel, this)); + + this._widgetDisposableStore.add(this.themeService.onDidColorThemeChange(() => { + if (this.isVisible()) { + this._updateInputDecoration(); + } + })); + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModelContent(() => { + if (this.isVisible()) { + this._updateInputDecoration(); + } + })); + + const cursorAtBoundaryContext = INTERACTIVE_INPUT_CURSOR_BOUNDARY.bindTo(this._contextKeyService); + if (input.resource && input.historyService.has(input.resource)) { + cursorAtBoundaryContext.set('top'); + } else { + cursorAtBoundaryContext.set('none'); + } + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeCursorPosition(({ position }) => { + const viewModel = this._codeEditorWidget._getViewModel()!; + const lastLineNumber = viewModel.getLineCount(); + const lastLineCol = viewModel.getLineLength(lastLineNumber) + 1; + const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position); + const firstLine = viewPosition.lineNumber === 1 && viewPosition.column === 1; + const lastLine = viewPosition.lineNumber === lastLineNumber && viewPosition.column === lastLineCol; + + if (firstLine) { + if (lastLine) { + cursorAtBoundaryContext.set('both'); + } else { + cursorAtBoundaryContext.set('top'); + } + } else { + if (lastLine) { + cursorAtBoundaryContext.set('bottom'); + } else { + cursorAtBoundaryContext.set('none'); + } + } + })); + + this._widgetDisposableStore.add(editorModel.onDidChangeContent(() => { + const value = editorModel.getValue(); + if (this.input?.resource && value !== '') { + (this.input as ReplEditorInput).historyService.replaceLast(this.input.resource, value); + } + })); + + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidScroll(() => this._onDidChangeScroll.fire())); + + this._syncWithKernel(); + } + + override setOptions(options: INotebookEditorOptions | undefined): void { + this._notebookWidget.value?.setOptions(options); + super.setOptions(options); + } + + private _toEditorPaneSelectionChangeReason(e: ICursorPositionChangedEvent): EditorPaneSelectionChangeReason { + switch (e.source) { + case TextEditorSelectionSource.PROGRAMMATIC: return EditorPaneSelectionChangeReason.PROGRAMMATIC; + case TextEditorSelectionSource.NAVIGATION: return EditorPaneSelectionChangeReason.NAVIGATION; + case TextEditorSelectionSource.JUMP: return EditorPaneSelectionChangeReason.JUMP; + default: return EditorPaneSelectionChangeReason.USER; + } + } + + private _cellAtBottom(cell: ICellViewModel): boolean { + const visibleRanges = this._notebookWidget.value?.visibleRanges || []; + const cellIndex = this._notebookWidget.value?.getCellIndex(cell); + if (cellIndex === Math.max(...visibleRanges.map(range => range.end - 1))) { + return true; + } + return false; + } + + private _scrollIfNecessary(cvm: ICellViewModel) { + const index = this._notebookWidget.value!.getCellIndex(cvm); + if (index === this._notebookWidget.value!.getLength() - 1) { + // If we're already at the bottom or auto scroll is enabled, scroll to the bottom + if (this._configurationService.getValue(InteractiveWindowSetting.interactiveWindowAlwaysScrollOnNewCell) || this._cellAtBottom(cvm)) { + this._notebookWidget.value!.scrollToBottom(); + } + } + } + + private _syncWithKernel() { + const notebook = this._notebookWidget.value?.textModel; + const textModel = this._codeEditorWidget.getModel(); + + if (notebook && textModel) { + const info = this._notebookKernelService.getMatchingKernel(notebook); + const selectedOrSuggested = info.selected + ?? (info.suggestions.length === 1 ? info.suggestions[0] : undefined) + ?? (info.all.length === 1 ? info.all[0] : undefined); + + if (selectedOrSuggested) { + const language = selectedOrSuggested.supportedLanguages[0]; + // All kernels will initially list plaintext as the supported language before they properly initialized. + if (language && language !== 'plaintext') { + const newMode = this._languageService.createById(language).languageId; + textModel.setLanguage(newMode); + } + + NOTEBOOK_KERNEL.bindTo(this._contextKeyService).set(selectedOrSuggested.id); + } + } + + this._updateInputDecoration(); + } + + layout(dimension: DOM.Dimension, position: DOM.IDomPosition): void { + this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); + this._rootElement.classList.toggle('narrow-width', dimension.width < 600); + const editorHeightChanged = dimension.height !== this._lastLayoutDimensions?.dimension.height; + this._lastLayoutDimensions = { dimension, position }; + + if (!this._notebookWidget.value) { + return; + } + + if (editorHeightChanged && this._codeEditorWidget) { + SuggestController.get(this._codeEditorWidget)?.cancelSuggestWidget(); + } + + this._notebookEditorContainer.style.height = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._layoutWidgets(dimension, position); + } + + private _layoutWidgets(dimension: DOM.Dimension, position: DOM.IDomPosition) { + const contentHeight = this._codeEditorWidget.hasModel() ? this._codeEditorWidget.getContentHeight() : this.inputCellEditorHeight; + const maxHeight = Math.min(dimension.height / 2, contentHeight); + const leftMargin = this._notebookOptions.getCellEditorContainerLeftMargin(); + + const inputCellContainerHeight = maxHeight + INPUT_CELL_VERTICAL_PADDING * 2; + this._notebookEditorContainer.style.height = `${dimension.height - inputCellContainerHeight}px`; + + this._notebookWidget.value!.layout(dimension.with(dimension.width, dimension.height - inputCellContainerHeight), this._notebookEditorContainer, position); + this._codeEditorWidget.layout(this._validateDimension(dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight)); + this._inputFocusIndicator.style.height = `${contentHeight}px`; + this._inputCellContainer.style.top = `${dimension.height - inputCellContainerHeight}px`; + this._inputCellContainer.style.width = `${dimension.width}px`; + } + + private _validateDimension(width: number, height: number) { + return new DOM.Dimension(Math.max(0, width), Math.max(0, height)); + } + + private _updateInputDecoration(): void { + if (!this._codeEditorWidget) { + return; + } + + if (!this._codeEditorWidget.hasModel()) { + return; + } + + const model = this._codeEditorWidget.getModel(); + + const decorations: IDecorationOptions[] = []; + + if (model?.getValueLength() === 0) { + const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4); + const languageId = model.getLanguageId(); + if (languageId !== 'plaintext') { + const keybinding = this._keybindingService.lookupKeybinding(EXECUTE_REPL_COMMAND_ID, this._contextKeyService)?.getLabel(); + const text = keybinding ? + nls.localize('interactiveInputPlaceHolder', "Type '{0}' code here and press {1} to run", languageId, keybinding) : + nls.localize('interactiveInputPlaceHolderNoKeybinding', "Type '{0}' code here and click run", languageId); + decorations.push({ + range: { + startLineNumber: 0, + endLineNumber: 0, + startColumn: 0, + endColumn: 1 + }, + renderOptions: { + after: { + contentText: text, + color: transparentForeground ? transparentForeground.toString() : undefined + } + } + }); + } + + } + + this._codeEditorWidget.setDecorationsByType('interactive-decoration', DECORATION_KEY, decorations); + } + + getScrollPosition(): IEditorPaneScrollPosition { + return { + scrollTop: this._notebookWidget.value?.scrollTop ?? 0, + scrollLeft: 0 + }; + } + + setScrollPosition(position: IEditorPaneScrollPosition): void { + this._notebookWidget.value?.setScrollTop(position.scrollTop); + } + + override focus() { + super.focus(); + + this._notebookWidget.value?.onShow(); + this._codeEditorWidget.focus(); + } + + focusHistory() { + this._notebookWidget.value!.focus(); + } + + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + this._groupListener.value = this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor)); + + if (!visible) { + this._saveEditorViewState(this.input); + if (this.input && this._notebookWidget.value) { + this._notebookWidget.value.onWillHide(); + } + } + } + + override clearInput() { + if (this._notebookWidget.value) { + this._saveEditorViewState(this.input); + this._notebookWidget.value.onWillHide(); + } + + this._codeEditorWidget?.dispose(); + + this._notebookWidget = { value: undefined }; + this._widgetDisposableStore.clear(); + + super.clearInput(); + } + + override getControl(): { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } { + return { + notebookEditor: this._notebookWidget.value, + codeEditor: this._codeEditorWidget + }; + } +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts b/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts new file mode 100644 index 00000000000..bf82d590a26 --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IReference } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { IInteractiveHistoryService } from 'vs/workbench/contrib/interactive/browser/interactiveHistoryService'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ICustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; + +export class ReplEditorInput extends NotebookEditorInput { + static override ID: string = 'workbench.editorinputs.replEditorInput'; + + private inputModelRef: IReference | undefined; + private isScratchpad: boolean; + private isDisposing = false; + + constructor( + resource: URI, + @INotebookService _notebookService: INotebookService, + @INotebookEditorModelResolverService _notebookModelResolverService: INotebookEditorModelResolverService, + @IFileDialogService _fileDialogService: IFileDialogService, + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IExtensionService extensionService: IExtensionService, + @IEditorService editorService: IEditorService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService, + @IInteractiveHistoryService public readonly historyService: IInteractiveHistoryService, + @ITextModelService private readonly _textModelService: ITextModelService, + @IConfigurationService configurationService: IConfigurationService + ) { + super(resource, undefined, 'jupyter-notebook', {}, _notebookService, _notebookModelResolverService, _fileDialogService, labelService, fileService, filesConfigurationService, extensionService, editorService, textResourceConfigurationService, customEditorLabelService); + this.isScratchpad = configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true; + } + + override get typeId(): string { + return ReplEditorInput.ID; + } + + override getName() { + return 'REPL'; + } + + override get capabilities() { + const capabilities = super.capabilities; + const scratchPad = this.isScratchpad ? EditorInputCapabilities.Scratchpad : 0; + + return capabilities + | EditorInputCapabilities.Readonly + | scratchPad; + } + + async resolveInput(notebook: NotebookTextModel) { + if (this.inputModelRef) { + return this.inputModelRef.object.textEditorModel; + } + + const lastCell = notebook.cells[notebook.cells.length - 1]; + this.inputModelRef = await this._textModelService.createModelReference(lastCell.uri); + return this.inputModelRef.object.textEditorModel; + } + + override dispose() { + if (!this.isDisposing) { + this.isDisposing = true; + this.editorModelReference?.object.revert({ soft: true }); + this.inputModelRef?.dispose(); + super.dispose(); + } + } +} diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 7ee1d516f6d..36a6aeffdb3 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { basename } from 'vs/base/common/resources'; -import { IDisposable, dispose, Disposable, DisposableStore, combinedDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; @@ -16,142 +16,153 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EditorResourceAccessor } from 'vs/workbench/common/editor'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { Schemas } from 'vs/base/common/network'; import { Iterable } from 'vs/base/common/iterator'; import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; import { IEditorGroupContextKeyProvider, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { getRepositoryResourceCount } from 'vs/workbench/contrib/scm/browser/util'; +import { autorun, autorunWithStore, derived, IObservable, observableFromEvent } from 'vs/base/common/observable'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; +import { derivedObservableWithCache, latestChangedValue, observableFromEventOpts } from 'vs/base/common/observableInternal/utils'; +import { Command } from 'vs/editor/common/languages'; +import { ISCMHistoryItemGroup } from 'vs/workbench/contrib/scm/common/history'; -function getCount(repository: ISCMRepository): number { - if (typeof repository.provider.count === 'number') { - return repository.provider.count; - } else { - return repository.provider.groups.reduce((r, g) => r + g.resources.length, 0); - } -} +const ActiveRepositoryContextKeys = { + ActiveRepositoryName: new RawContextKey('scmActiveRepositoryName', ''), + ActiveRepositoryBranchName: new RawContextKey('scmActiveRepositoryBranchName', ''), +}; -export class SCMStatusController implements IWorkbenchContribution { +export class SCMActiveRepositoryController extends Disposable implements IWorkbenchContribution { + private readonly _countBadgeConfig = observableConfigValue<'all' | 'focused' | 'off'>('scm.countBadge', 'all', this.configurationService); - private statusBarDisposable: IDisposable = Disposable.None; - private focusDisposable: IDisposable = Disposable.None; - private focusedRepository: ISCMRepository | undefined = undefined; - private readonly badgeDisposable = new MutableDisposable(); - private readonly disposables = new DisposableStore(); - private repositoryDisposables = new Set(); + private readonly _repositories = observableFromEvent(this, + Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), + () => this.scmService.repositories); + + private readonly _focusedRepository = observableFromEventOpts( + { owner: this, equalsFn: () => false }, + this.scmViewService.onDidFocusRepository, + () => this.scmViewService.focusedRepository); + + private readonly _activeEditor = observableFromEventOpts( + { owner: this, equalsFn: () => false }, + this.editorService.onDidActiveEditorChange, + () => this.editorService.activeEditor); + + private readonly _activeEditorRepository = derivedObservableWithCache(this, (reader, lastValue) => { + const activeResource = EditorResourceAccessor.getOriginalUri(this._activeEditor.read(reader)); + if (!activeResource) { + return lastValue; + } + + const repository = this.scmService.getRepository(activeResource); + if (!repository) { + return lastValue; + } + + return Object.create(repository); + }); + + /** + * The focused repository takes precedence over the active editor repository when the observable + * values are updated in the same transaction (or during the initial read of the observable value). + */ + private readonly _activeRepository = latestChangedValue(this, [this._activeEditorRepository, this._focusedRepository]); + + private readonly _countBadgeRepositories = derived(this, reader => { + switch (this._countBadgeConfig.read(reader)) { + case 'all': { + const repositories = this._repositories.read(reader); + return [...Iterable.map(repositories, r => ({ ...r.provider, resourceCount: this._getRepositoryResourceCount(r) }))]; + } + case 'focused': { + const repository = this._activeRepository.read(reader); + return repository ? [{ ...repository.provider, resourceCount: this._getRepositoryResourceCount(repository) }] : []; + } + case 'off': + return []; + default: + throw new Error('Invalid countBadge setting'); + } + }); + + private readonly _countBadge = derived(this, reader => { + let total = 0; + + for (const repository of this._countBadgeRepositories.read(reader)) { + const count = repository.count?.read(reader); + const resourceCount = repository.resourceCount.read(reader); + + total = total + (count ?? resourceCount); + } + + return total; + }); + + private _activeRepositoryNameContextKey: IContextKey; + private _activeRepositoryBranchNameContextKey: IContextKey; constructor( + @IActivityService private readonly activityService: IActivityService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IEditorService private readonly editorService: IEditorService, @ISCMService private readonly scmService: ISCMService, @ISCMViewService private readonly scmViewService: ISCMViewService, @IStatusbarService private readonly statusbarService: IStatusbarService, - @IActivityService private readonly activityService: IActivityService, - @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @ITitleService private readonly titleService: ITitleService ) { - this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + super(); - const onDidChangeSCMCountBadge = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.countBadge')); - onDidChangeSCMCountBadge(this.renderActivityCount, this, this.disposables); + this._activeRepositoryNameContextKey = ActiveRepositoryContextKeys.ActiveRepositoryName.bindTo(this.contextKeyService); + this._activeRepositoryBranchNameContextKey = ActiveRepositoryContextKeys.ActiveRepositoryBranchName.bindTo(this.contextKeyService); - for (const repository of this.scmService.repositories) { - this.onDidAddRepository(repository); - } + this.titleService.registerVariables([ + { name: 'activeRepositoryName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryName.key }, + { name: 'activeRepositoryBranchName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryBranchName.key, } + ]); - this.scmViewService.onDidFocusRepository(this.focusRepository, this, this.disposables); - this.focusRepository(this.scmViewService.focusedRepository); + this._register(autorunWithStore((reader, store) => { + this._updateActivityCountBadge(this._countBadge.read(reader), store); + })); - editorService.onDidActiveEditorChange(() => this.tryFocusRepositoryBasedOnActiveEditor(), this, this.disposables); - this.renderActivityCount(); + this._register(autorunWithStore((reader, store) => { + const repository = this._activeRepository.read(reader); + const commands = repository?.provider.statusBarCommands.read(reader); + + this._updateStatusBar(repository, commands ?? [], store); + })); + + this._register(autorun(reader => { + const repository = this._activeRepository.read(reader); + const currentHistoryItemGroup = repository?.provider.historyProviderObs.read(reader)?.currentHistoryItemGroupObs.read(reader); + + this._updateActiveRepositoryContextKeys(repository, currentHistoryItemGroup); + })); } - private tryFocusRepositoryBasedOnActiveEditor(repositories: Iterable = this.scmService.repositories): boolean { - const resource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); - - if (!resource) { - return false; - } - - let bestRepository: ISCMRepository | null = null; - let bestMatchLength = Number.POSITIVE_INFINITY; - - for (const repository of repositories) { - const root = repository.provider.rootUri; - - if (!root) { - continue; - } - - const path = this.uriIdentityService.extUri.relativePath(root, resource); - - if (path && !/^\.\./.test(path) && path.length < bestMatchLength) { - bestRepository = repository; - bestMatchLength = path.length; - } - } - - if (!bestRepository) { - return false; - } - - this.focusRepository(bestRepository); - return true; + private _getRepositoryResourceCount(repository: ISCMRepository): IObservable { + return observableFromEvent(this, repository.provider.onDidChangeResources, () => /** @description repositoryResourceCount */ getRepositoryResourceCount(repository.provider)); } - private onDidAddRepository(repository: ISCMRepository): void { - const onDidChange = Event.any(repository.provider.onDidChange, repository.provider.onDidChangeResources); - const changeDisposable = onDidChange(() => this.renderActivityCount()); - - const onDidRemove = Event.filter(this.scmService.onDidRemoveRepository, e => e === repository); - const removeDisposable = onDidRemove(() => { - disposable.dispose(); - this.repositoryDisposables.delete(disposable); - this.renderActivityCount(); - }); - - const disposable = combinedDisposable(changeDisposable, removeDisposable); - this.repositoryDisposables.add(disposable); - - this.tryFocusRepositoryBasedOnActiveEditor(Iterable.single(repository)); - } - - private onDidRemoveRepository(repository: ISCMRepository): void { - if (this.focusedRepository !== repository) { + private _updateActivityCountBadge(count: number, store: DisposableStore): void { + if (count === 0) { return; } - this.focusRepository(Iterable.first(this.scmService.repositories)); + const badge = new NumberBadge(count, num => localize('scmPendingChangesBadge', '{0} pending changes', num)); + store.add(this.activityService.showViewActivity(VIEW_PANE_ID, { badge })); } - private focusRepository(repository: ISCMRepository | undefined): void { - if (this.focusedRepository === repository) { - return; - } - - this.focusDisposable.dispose(); - this.focusedRepository = repository; - - if (repository && repository.provider.onDidChangeStatusBarCommands) { - this.focusDisposable = repository.provider.onDidChangeStatusBarCommands(() => this.renderStatusBar(repository)); - } - - this.renderStatusBar(repository); - this.renderActivityCount(); - } - - private renderStatusBar(repository: ISCMRepository | undefined): void { - this.statusBarDisposable.dispose(); - + private _updateStatusBar(repository: ISCMRepository | undefined, commands: readonly Command[], store: DisposableStore): void { if (!repository) { return; } - const commands = repository.provider.statusBarCommands || []; const label = repository.provider.rootUri ? `${basename(repository.provider.rootUri)} (${repository.provider.label})` : repository.provider.label; - const disposables = new DisposableStore(); for (let index = 0; index < commands.length; index++) { const command = commands[index]; const tooltip = `${label}${command.tooltip ? ` - ${command.tooltip}` : ''}`; @@ -178,213 +189,91 @@ export class SCMStatusController implements IWorkbenchContribution { command: command.id ? command : undefined }; - disposables.add(index === 0 ? + store.add(index === 0 ? this.statusbarService.addEntry(statusbarEntry, `status.scm.${index}`, MainThreadStatusBarAlignment.LEFT, 10000) : this.statusbarService.addEntry(statusbarEntry, `status.scm.${index}`, MainThreadStatusBarAlignment.LEFT, { id: `status.scm.${index - 1}`, alignment: MainThreadStatusBarAlignment.RIGHT, compact: true }) ); } - - this.statusBarDisposable = disposables; } - private renderActivityCount(): void { - const countBadgeType = this.configurationService.getValue<'all' | 'focused' | 'off'>('scm.countBadge'); - - let count = 0; - - if (countBadgeType === 'all') { - count = Iterable.reduce(this.scmService.repositories, (r, repository) => r + getCount(repository), 0); - } else if (countBadgeType === 'focused' && this.focusedRepository) { - count = getCount(this.focusedRepository); - } - - if (count > 0) { - const badge = new NumberBadge(count, num => localize('scmPendingChangesBadge', '{0} pending changes', num)); - this.badgeDisposable.value = this.activityService.showViewActivity(VIEW_PANE_ID, { badge }); - } else { - this.badgeDisposable.value = undefined; - } - } - - dispose(): void { - this.focusDisposable.dispose(); - this.statusBarDisposable.dispose(); - this.badgeDisposable.dispose(); - this.disposables.dispose(); - dispose(this.repositoryDisposables.values()); - this.repositoryDisposables.clear(); + private _updateActiveRepositoryContextKeys(repository: ISCMRepository | undefined, currentHistoryItemGroup: ISCMHistoryItemGroup | undefined): void { + this._activeRepositoryNameContextKey.set(repository?.provider.name ?? ''); + this._activeRepositoryBranchNameContextKey.set(currentHistoryItemGroup?.name ?? ''); } } -const ActiveRepositoryContextKeys = { - ActiveRepositoryName: new RawContextKey('scmActiveRepositoryName', ''), - ActiveRepositoryBranchName: new RawContextKey('scmActiveRepositoryBranchName', ''), -}; +export class SCMActiveResourceContextKeyController extends Disposable implements IWorkbenchContribution { + private readonly _repositories = observableFromEvent(this, + Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), + () => this.scmService.repositories); -export class SCMActiveRepositoryContextKeyController implements IWorkbenchContribution { - - private activeRepositoryNameContextKey: IContextKey; - private activeRepositoryBranchNameContextKey: IContextKey; - - private focusedRepository: ISCMRepository | undefined = undefined; - private focusDisposable: IDisposable = Disposable.None; - private readonly disposables = new DisposableStore(); - - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @IEditorService private readonly editorService: IEditorService, - @ISCMViewService private readonly scmViewService: ISCMViewService, - @ITitleService titleService: ITitleService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService - ) { - this.activeRepositoryNameContextKey = ActiveRepositoryContextKeys.ActiveRepositoryName.bindTo(contextKeyService); - this.activeRepositoryBranchNameContextKey = ActiveRepositoryContextKeys.ActiveRepositoryBranchName.bindTo(contextKeyService); - - titleService.registerVariables([ - { name: 'activeRepositoryName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryName.key }, - { name: 'activeRepositoryBranchName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryBranchName.key, } - ]); - - editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.disposables); - scmViewService.onDidFocusRepository(this.onDidFocusRepository, this, this.disposables); - this.onDidFocusRepository(scmViewService.focusedRepository); - } - - private onDidActiveEditorChange(): void { - const activeResource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); - - if (activeResource?.scheme !== Schemas.file && activeResource?.scheme !== Schemas.vscodeRemote) { - return; - } - - const repository = Iterable.find( - this.scmViewService.repositories, - r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)) - ); - - this.onDidFocusRepository(repository); - } - - private onDidFocusRepository(repository: ISCMRepository | undefined): void { - if (!repository || this.focusedRepository === repository) { - return; - } - - this.focusDisposable.dispose(); - this.focusedRepository = repository; - - if (repository && repository.provider.onDidChangeStatusBarCommands) { - this.focusDisposable = repository.provider.onDidChangeStatusBarCommands(() => this.updateContextKeys(repository)); - } - - this.updateContextKeys(repository); - } - - private updateContextKeys(repository: ISCMRepository | undefined): void { - this.activeRepositoryNameContextKey.set(repository?.provider.name ?? ''); - this.activeRepositoryBranchNameContextKey.set(repository?.provider.historyProvider?.currentHistoryItemGroup?.name ?? ''); - } - - dispose(): void { - this.focusDisposable.dispose(); - this.disposables.dispose(); - } -} - -export class SCMActiveResourceContextKeyController implements IWorkbenchContribution { - - private readonly disposables = new DisposableStore(); - private repositoryDisposables = new Set(); - private onDidRepositoryChange = new Emitter(); + private readonly _onDidRepositoryChange = new Emitter(); constructor( @IEditorGroupsService editorGroupsService: IEditorGroupsService, @ISCMService private readonly scmService: ISCMService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { + super(); + const activeResourceHasChangesContextKey = new RawContextKey('scmActiveResourceHasChanges', false, localize('scmActiveResourceHasChanges', "Whether the active resource has changes")); const activeResourceRepositoryContextKey = new RawContextKey('scmActiveResourceRepository', undefined, localize('scmActiveResourceRepository', "The active resource's repository")); - this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - - for (const repository of this.scmService.repositories) { - this.onDidAddRepository(repository); - } + this._store.add(autorunWithStore((reader, store) => { + for (const repository of this._repositories.read(reader)) { + store.add(Event.runAndSubscribe(repository.provider.onDidChangeResources, () => { + this._onDidRepositoryChange.fire(); + })); + } + })); // Create context key providers which will update the context keys based on each groups active editor const hasChangesContextKeyProvider: IEditorGroupContextKeyProvider = { contextKey: activeResourceHasChangesContextKey, - getGroupContextKeyValue: (group) => this.getEditorHasChanges(group.activeEditor), - onDidChange: this.onDidRepositoryChange.event + getGroupContextKeyValue: (group) => this._getEditorHasChanges(group.activeEditor), + onDidChange: this._onDidRepositoryChange.event }; const repositoryContextKeyProvider: IEditorGroupContextKeyProvider = { contextKey: activeResourceRepositoryContextKey, - getGroupContextKeyValue: (group) => this.getEditorRepositoryId(group.activeEditor), - onDidChange: this.onDidRepositoryChange.event + getGroupContextKeyValue: (group) => this._getEditorRepositoryId(group.activeEditor), + onDidChange: this._onDidRepositoryChange.event }; - this.disposables.add(editorGroupsService.registerContextKeyProvider(hasChangesContextKeyProvider)); - this.disposables.add(editorGroupsService.registerContextKeyProvider(repositoryContextKeyProvider)); + this._store.add(editorGroupsService.registerContextKeyProvider(hasChangesContextKeyProvider)); + this._store.add(editorGroupsService.registerContextKeyProvider(repositoryContextKeyProvider)); } - private onDidAddRepository(repository: ISCMRepository): void { - const onDidChange = Event.any(repository.provider.onDidChange, repository.provider.onDidChangeResources); - const changeDisposable = onDidChange(() => { - this.onDidRepositoryChange.fire(); - }); - - const onDidRemove = Event.filter(this.scmService.onDidRemoveRepository, e => e === repository); - const removeDisposable = onDidRemove(() => { - disposable.dispose(); - this.repositoryDisposables.delete(disposable); - this.onDidRepositoryChange.fire(); - }); - - const disposable = combinedDisposable(changeDisposable, removeDisposable); - this.repositoryDisposables.add(disposable); - } - - private getEditorRepositoryId(activeEditor: EditorInput | null): string | undefined { + private _getEditorHasChanges(activeEditor: EditorInput | null): boolean { const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor); - - if (activeResource?.scheme === Schemas.file || activeResource?.scheme === Schemas.vscodeRemote) { - const activeResourceRepository = Iterable.find( - this.scmService.repositories, - r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)) - ); - - return activeResourceRepository?.id; + if (!activeResource) { + return false; } - return undefined; - } - - private getEditorHasChanges(activeEditor: EditorInput | null): boolean { - const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor); - - if (activeResource?.scheme === Schemas.file || activeResource?.scheme === Schemas.vscodeRemote) { - const activeResourceRepository = Iterable.find( - this.scmService.repositories, - r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)) - ); - - for (const resourceGroup of activeResourceRepository?.provider.groups ?? []) { - if (resourceGroup.resources - .some(scmResource => - this.uriIdentityService.extUri.isEqual(activeResource, scmResource.sourceUri))) { - return true; - } + const activeResourceRepository = this.scmService.getRepository(activeResource); + for (const resourceGroup of activeResourceRepository?.provider.groups ?? []) { + if (resourceGroup.resources + .some(scmResource => + this.uriIdentityService.extUri.isEqual(activeResource, scmResource.sourceUri))) { + return true; } } return false; } - dispose(): void { - this.disposables.dispose(); - dispose(this.repositoryDisposables.values()); - this.repositoryDisposables.clear(); - this.onDidRepositoryChange.dispose(); + private _getEditorRepositoryId(activeEditor: EditorInput | null): string | undefined { + const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor); + if (!activeResource) { + return undefined; + } + + const activeResourceRepository = this.scmService.getRepository(activeResource); + return activeResourceRepository?.id; + } + + override dispose(): void { + this._onDidRepositoryChange.dispose(); + super.dispose(); } } diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 7537f80f534..173cdb04fd0 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -1013,37 +1013,17 @@ const editorGutterAddedBackground = registerColor('editorGutter.addedBackground' hcLight: '#48985D' }, nls.localize('editorGutterAddedBackground', "Editor gutter background color for lines that are added.")); -const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', { - dark: editorErrorForeground, - light: editorErrorForeground, - hcDark: editorErrorForeground, - hcLight: editorErrorForeground -}, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted.")); +const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', editorErrorForeground, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted.")); -const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', { - dark: editorGutterModifiedBackground, - light: editorGutterModifiedBackground, - hcDark: editorGutterModifiedBackground, - hcLight: editorGutterModifiedBackground -}, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified.")); +const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', editorGutterModifiedBackground, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified.")); -const minimapGutterAddedBackground = registerColor('minimapGutter.addedBackground', { - dark: editorGutterAddedBackground, - light: editorGutterAddedBackground, - hcDark: editorGutterAddedBackground, - hcLight: editorGutterAddedBackground -}, nls.localize('minimapGutterAddedBackground', "Minimap gutter background color for lines that are added.")); +const minimapGutterAddedBackground = registerColor('minimapGutter.addedBackground', editorGutterAddedBackground, nls.localize('minimapGutterAddedBackground', "Minimap gutter background color for lines that are added.")); -const minimapGutterDeletedBackground = registerColor('minimapGutter.deletedBackground', { - dark: editorGutterDeletedBackground, - light: editorGutterDeletedBackground, - hcDark: editorGutterDeletedBackground, - hcLight: editorGutterDeletedBackground -}, nls.localize('minimapGutterDeletedBackground', "Minimap gutter background color for lines that are deleted.")); +const minimapGutterDeletedBackground = registerColor('minimapGutter.deletedBackground', editorGutterDeletedBackground, nls.localize('minimapGutterDeletedBackground', "Minimap gutter background color for lines that are deleted.")); -const overviewRulerModifiedForeground = registerColor('editorOverviewRuler.modifiedForeground', { dark: transparent(editorGutterModifiedBackground, 0.6), light: transparent(editorGutterModifiedBackground, 0.6), hcDark: transparent(editorGutterModifiedBackground, 0.6), hcLight: transparent(editorGutterModifiedBackground, 0.6) }, nls.localize('overviewRulerModifiedForeground', 'Overview ruler marker color for modified content.')); -const overviewRulerAddedForeground = registerColor('editorOverviewRuler.addedForeground', { dark: transparent(editorGutterAddedBackground, 0.6), light: transparent(editorGutterAddedBackground, 0.6), hcDark: transparent(editorGutterAddedBackground, 0.6), hcLight: transparent(editorGutterAddedBackground, 0.6) }, nls.localize('overviewRulerAddedForeground', 'Overview ruler marker color for added content.')); -const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.deletedForeground', { dark: transparent(editorGutterDeletedBackground, 0.6), light: transparent(editorGutterDeletedBackground, 0.6), hcDark: transparent(editorGutterDeletedBackground, 0.6), hcLight: transparent(editorGutterDeletedBackground, 0.6) }, nls.localize('overviewRulerDeletedForeground', 'Overview ruler marker color for deleted content.')); +const overviewRulerModifiedForeground = registerColor('editorOverviewRuler.modifiedForeground', transparent(editorGutterModifiedBackground, 0.6), nls.localize('overviewRulerModifiedForeground', 'Overview ruler marker color for modified content.')); +const overviewRulerAddedForeground = registerColor('editorOverviewRuler.addedForeground', transparent(editorGutterAddedBackground, 0.6), nls.localize('overviewRulerAddedForeground', 'Overview ruler marker color for added content.')); +const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.deletedForeground', transparent(editorGutterDeletedBackground, 0.6), nls.localize('overviewRulerDeletedForeground', 'Overview ruler marker color for deleted content.')); class DirtyDiffDecorator extends Disposable { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index c8939c63fbe..288452b11c0 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -67,10 +67,6 @@ justify-content: flex-end; } -.scm-view .monaco-editor .selected-text { - border-radius: 0; -} - /** * The following rules are very specific because of inline drop down menus * https://github.com/microsoft/vscode/issues/101410 @@ -133,6 +129,31 @@ align-items: center; } +.scm-view .monaco-list-row .history-item > .graph-container { + display: flex; + flex-shrink: 0; + height: 22px; +} + +.scm-view .monaco-list-row .history-item > .graph-container > .graph > circle { + stroke: var(--vscode-sideBar-background); +} + +.scm-view .monaco-list-row .history-item > .label-container { + display: flex; + opacity: 0.75; + flex-shrink: 0; + gap: 4px; +} + +.scm-view .monaco-list-row .history-item > .label-container > .codicon { + font-size: 14px; + border: 1px solid var(--vscode-scm-historyItemStatisticsBorder); + border-radius: 2px; + margin: 1px 0; + padding: 2px +} + .scm-view .monaco-list-row .history-item .stats-container { display: flex; font-size: 11px; @@ -332,10 +353,6 @@ border-radius: 2px; } -.scm-view .scm-editor-container .monaco-editor .focused .selected-text { - background-color: var(--vscode-editor-selectionBackground); -} - .scm-view .scm-editor { box-sizing: border-box; width: 100%; @@ -475,22 +492,6 @@ margin-top: 1px; } -.scm-view .scm-editor-placeholder { - position: absolute; - pointer-events: none; - z-index: 1; - padding: 2px 6px; - box-sizing: border-box; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - color: var(--vscode-input-placeholderForeground); -} - -.scm-view .scm-editor-placeholder.hidden { - display: none; -} - .scm-view .scm-editor-container .monaco-editor-background, .scm-view .scm-editor-container .monaco-editor, .scm-view .scm-editor-container .monaco-editor .margin, @@ -505,6 +506,10 @@ color: var(--vscode-input-foreground); } +.scm-view .scm-editor-container .placeholder-text.mtk1 { + color: var(--vscode-input-placeholderForeground); +} + /* Repositories */ .scm-repositories-view .scm-provider { diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index 39ee31783bd..a5719651927 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -199,7 +199,7 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { ]); const serviceCollection = new ServiceCollection([IContextKeyService, this.contextKeyService]); - instantiationService = instantiationService.createChild(serviceCollection); + instantiationService = instantiationService.createChild(serviceCollection, this.disposables); this.titleMenu = instantiationService.createInstance(SCMTitleMenu); this.disposables.add(this.titleMenu); @@ -306,7 +306,7 @@ export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDispo private getOutgoingHistoryItemGroupMenu(menuId: MenuId, historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu { const contextKeyService = this.contextKeyService.createOverlay([ - ['scmHistoryItemGroupHasUpstream', !!historyItemGroup.repository.provider.historyProvider?.currentHistoryItemGroup?.base], + ['scmHistoryItemGroupHasRemote', !!historyItemGroup.repository.provider.historyProvider?.currentHistoryItemGroup?.remote], ]); return this.menuService.createMenu(menuId, contextKeyService); diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 70892dfb799..91129c1d6ca 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -10,7 +10,7 @@ import { DirtyDiffWorkbenchController } from './dirtydiffDecorator'; import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { SCMActiveRepositoryContextKeyController, SCMActiveResourceContextKeyController, SCMStatusController } from './activity'; +import { SCMActiveResourceContextKeyController, SCMActiveRepositoryController } from './activity'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -24,7 +24,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -import { SCMViewPane } from 'vs/workbench/contrib/scm/browser/scmViewPane'; +import { ContextKeys, SCMViewPane } from 'vs/workbench/contrib/scm/browser/scmViewPane'; import { SCMViewService } from 'vs/workbench/contrib/scm/browser/scmViewService'; import { SCMRepositoriesViewPane } from 'vs/workbench/contrib/scm/browser/scmRepositoriesViewPane'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -34,6 +34,7 @@ import { IQuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiff'; import { QuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiffService'; import { getActiveElement } from 'vs/base/browser/dom'; import { SCMWorkingSetController } from 'vs/workbench/contrib/scm/browser/workingSet'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; ModesRegistry.registerLanguage({ id: 'scminput', @@ -111,15 +112,12 @@ viewsRegistry.registerViews([{ containerIcon: sourceControlViewIcon }], viewContainer); +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(SCMActiveRepositoryController, LifecyclePhase.Restored); + Registry.as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(SCMActiveResourceContextKeyController, LifecyclePhase.Restored); -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(SCMActiveRepositoryContextKeyController, LifecyclePhase.Restored); - -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(SCMStatusController, LifecyclePhase.Restored); - registerWorkbenchContribution2( SCMWorkingSetController.ID, SCMWorkingSetController, @@ -351,6 +349,11 @@ Registry.as(ConfigurationExtensions.Configuration).regis ], description: localize('scm.workingSets.default', "Controls the default working set to use when switching to a source control history item group that does not have a working set."), default: 'current' + }, + 'scm.experimental.showHistoryGraph': { + type: 'boolean', + description: localize('scm.experimental.showHistoryGraph', "Controls whether to show the history graph instead of incoming/outgoing changes in the Source Control view."), + default: false } } }); @@ -385,6 +388,22 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'scm.clearInput', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(ContextKeyExpr.has('scmRepository'), SuggestContext.Visible.toNegated()), + primary: KeyCode.Escape, + handler: async (accessor) => { + const scmService = accessor.get(ISCMService); + const contextKeyService = accessor.get(IContextKeyService); + + const context = contextKeyService.getContext(getActiveElement()); + const repositoryId = context.getValue('scmRepository'); + const repository = repositoryId ? scmService.getRepository(repositoryId) : undefined; + repository?.input.setValue('', true); + } +}); + const viewNextCommitCommand = { description: { description: localize('scm view next commit', "Source Control: View Next Commit"), args: [] }, weight: KeybindingWeight.WorkbenchContrib, @@ -475,6 +494,56 @@ MenuRegistry.appendMenuItem(MenuId.SCMSourceControl, { when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProviderHasRootUri', true), ContextKeyExpr.or(ContextKeyExpr.equals('config.terminal.sourceControlRepositoriesKind', 'integrated'), ContextKeyExpr.equals('config.terminal.sourceControlRepositoriesKind', 'both'))) }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.scm.action.focusPreviousInput', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeys.RepositoryVisibilityCount.notEqualsTo(0), + handler: async accessor => { + const viewsService = accessor.get(IViewsService); + const scmView = await viewsService.openView(VIEW_PANE_ID); + if (scmView) { + scmView.focusPreviousInput(); + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.scm.action.focusNextInput', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeys.RepositoryVisibilityCount.notEqualsTo(0), + handler: async accessor => { + const viewsService = accessor.get(IViewsService); + const scmView = await viewsService.openView(VIEW_PANE_ID); + if (scmView) { + scmView.focusNextInput(); + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.scm.action.focusPreviousResourceGroup', + weight: KeybindingWeight.WorkbenchContrib, + handler: async accessor => { + const viewsService = accessor.get(IViewsService); + const scmView = await viewsService.openView(VIEW_PANE_ID); + if (scmView) { + scmView.focusPreviousResourceGroup(); + } + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.scm.action.focusNextResourceGroup', + weight: KeybindingWeight.WorkbenchContrib, + handler: async accessor => { + const viewsService = accessor.get(IViewsService); + const scmView = await viewsService.openView(VIEW_PANE_ID); + if (scmView) { + scmView.focusNextResourceGroup(); + } + } +}); + registerSingleton(ISCMService, SCMService, InstantiationType.Delayed); registerSingleton(ISCMViewService, SCMViewService, InstantiationType.Delayed); registerSingleton(IQuickDiffService, QuickDiffService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts new file mode 100644 index 00000000000..f6505a2eec4 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -0,0 +1,261 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { lastOrDefault } from 'vs/base/common/arrays'; +import { deepClone } from 'vs/base/common/objects'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemViewModel } from 'vs/workbench/contrib/scm/common/history'; + +const SWIMLANE_HEIGHT = 22; +const SWIMLANE_WIDTH = 11; +const CIRCLE_RADIUS = 4; +const SWIMLANE_CURVE_RADIUS = 5; + +const graphColors = ['#007ACC', '#BC3FBC', '#BF8803', '#CC6633', '#F14C4C', '#16825D']; + +function getNextColorIndex(colorIndex: number): number { + return colorIndex < graphColors.length - 1 ? colorIndex + 1 : 1; +} + +function getLabelColorIndex(historyItem: ISCMHistoryItem, colorMap: Map): number | undefined { + for (const label of historyItem.labels ?? []) { + const colorIndex = colorMap.get(label.title); + if (colorIndex !== undefined) { + return colorIndex; + } + } + + return undefined; +} + +function createPath(stroke: string): SVGPathElement { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('fill', 'none'); + path.setAttribute('stroke', stroke); + path.setAttribute('stroke-width', '1px'); + path.setAttribute('stroke-linecap', 'round'); + + return path; +} + +function drawCircle(index: number, radius: number, fill: string): SVGCircleElement { + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + circle.setAttribute('cx', `${SWIMLANE_WIDTH * (index + 1)}`); + circle.setAttribute('cy', `${SWIMLANE_WIDTH}`); + circle.setAttribute('r', `${radius}`); + circle.setAttribute('fill', fill); + + return circle; +} + +function drawVerticalLine(x1: number, y1: number, y2: number, color: string): SVGPathElement { + const path = createPath(color); + path.setAttribute('d', `M ${x1} ${y1} V ${y2}`); + + return path; +} + +function findLastIndex(nodes: ISCMHistoryItemGraphNode[], id: string): number { + for (let i = nodes.length - 1; i >= 0; i--) { + if (nodes[i].id === id) { + return i; + } + } + + return -1; +} + +export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemViewModel): SVGElement { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.classList.add('graph'); + + const historyItem = historyItemViewModel.historyItem; + const inputSwimlanes = historyItemViewModel.inputSwimlanes; + const outputSwimlanes = historyItemViewModel.outputSwimlanes; + + // Find the history item in the input swimlanes + const inputIndex = inputSwimlanes.findIndex(node => node.id === historyItem.id); + + // Circle index - use the input swimlane index if present, otherwise add it to the end + const circleIndex = inputIndex !== -1 ? inputIndex : inputSwimlanes.length; + + // Circle color - use the output swimlane color if present, otherwise the input swimlane color + const circleColorIndex = circleIndex < outputSwimlanes.length ? outputSwimlanes[circleIndex].color : inputSwimlanes[circleIndex].color; + + let outputSwimlaneIndex = 0; + for (let index = 0; index < inputSwimlanes.length; index++) { + const color = graphColors[inputSwimlanes[index].color]; + + // Current commit + if (inputSwimlanes[index].id === historyItem.id) { + // Base commit + if (index !== circleIndex) { + const d: string[] = []; + const path = createPath(color); + + // Draw / + d.push(`M ${SWIMLANE_WIDTH * (index + 1)} 0`); + d.push(`A ${SWIMLANE_WIDTH} ${SWIMLANE_WIDTH} 0 0 1 ${SWIMLANE_WIDTH * (index)} ${SWIMLANE_WIDTH}`); + + // Draw - + d.push(`H ${SWIMLANE_WIDTH * (circleIndex + 1)}`); + + path.setAttribute('d', d.join(' ')); + svg.append(path); + } else { + outputSwimlaneIndex++; + } + } else { + // Not the current commit + if (outputSwimlaneIndex < outputSwimlanes.length && + inputSwimlanes[index].id === outputSwimlanes[outputSwimlaneIndex].id) { + if (index === outputSwimlaneIndex) { + // Draw | + const path = drawVerticalLine(SWIMLANE_WIDTH * (index + 1), 0, SWIMLANE_HEIGHT, color); + svg.append(path); + } else { + const d: string[] = []; + const path = createPath(color); + + // Draw | + d.push(`M ${SWIMLANE_WIDTH * (index + 1)} 0`); + d.push(`V 6`); + + // Draw / + d.push(`A ${SWIMLANE_CURVE_RADIUS} ${SWIMLANE_CURVE_RADIUS} 0 0 1 ${(SWIMLANE_WIDTH * (index + 1)) - SWIMLANE_CURVE_RADIUS} ${SWIMLANE_HEIGHT / 2}`); + + // Draw - + d.push(`H ${(SWIMLANE_WIDTH * (outputSwimlaneIndex + 1)) + SWIMLANE_CURVE_RADIUS}`); + + // Draw / + d.push(`A ${SWIMLANE_CURVE_RADIUS} ${SWIMLANE_CURVE_RADIUS} 0 0 0 ${SWIMLANE_WIDTH * (outputSwimlaneIndex + 1)} ${(SWIMLANE_HEIGHT / 2) + SWIMLANE_CURVE_RADIUS}`); + + // Draw | + d.push(`V ${SWIMLANE_HEIGHT}`); + + path.setAttribute('d', d.join(' ')); + svg.append(path); + } + + outputSwimlaneIndex++; + } + } + } + + // Add remaining parent(s) + for (let i = 1; i < historyItem.parentIds.length; i++) { + const parentOutputIndex = findLastIndex(outputSwimlanes, historyItem.parentIds[i]); + if (parentOutputIndex === -1) { + continue; + } + + // Draw -\ + const d: string[] = []; + const path = createPath(graphColors[outputSwimlanes[parentOutputIndex].color]); + + // Draw \ + d.push(`M ${SWIMLANE_WIDTH * parentOutputIndex} ${SWIMLANE_HEIGHT / 2}`); + d.push(`A ${SWIMLANE_WIDTH} ${SWIMLANE_WIDTH} 0 0 1 ${SWIMLANE_WIDTH * (parentOutputIndex + 1)} ${SWIMLANE_HEIGHT}`); + + // Draw - + d.push(`M ${SWIMLANE_WIDTH * parentOutputIndex} ${SWIMLANE_HEIGHT / 2}`); + d.push(`H ${SWIMLANE_WIDTH * (circleIndex + 1)} `); + + path.setAttribute('d', d.join(' ')); + svg.append(path); + } + + // Draw | to * + if (inputIndex !== -1) { + const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), 0, SWIMLANE_HEIGHT / 2, graphColors[inputSwimlanes[inputIndex].color]); + svg.append(path); + } + + // Draw | from * + if (historyItem.parentIds.length > 0) { + const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), SWIMLANE_HEIGHT / 2, SWIMLANE_HEIGHT, graphColors[circleColorIndex]); + svg.append(path); + } + + // Draw * + if (historyItem.parentIds.length > 1) { + // Multi-parent node + const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, graphColors[circleColorIndex]); + svg.append(circleOuter); + + const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, graphColors[circleColorIndex]); + svg.append(circleInner); + } else { + // HEAD + // TODO@lszomoru - implement a better way to determine if the commit is HEAD + if (historyItem.labels?.some(l => ThemeIcon.isThemeIcon(l.icon) && l.icon.id === 'target')) { + const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 2, graphColors[circleColorIndex]); + svg.append(outerCircle); + } + + // Node + const circle = drawCircle(circleIndex, CIRCLE_RADIUS, graphColors[circleColorIndex]); + svg.append(circle); + } + + // Set dimensions + svg.style.height = `${SWIMLANE_HEIGHT}px`; + svg.style.width = `${SWIMLANE_WIDTH * (Math.max(inputSwimlanes.length, outputSwimlanes.length, 1) + 1)}px`; + + return svg; +} + +export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map()): ISCMHistoryItemViewModel[] { + let colorIndex = -1; + const viewModels: ISCMHistoryItemViewModel[] = []; + + for (let index = 0; index < historyItems.length; index++) { + const historyItem = historyItems[index]; + + const outputSwimlanesFromPreviousItem = lastOrDefault(viewModels)?.outputSwimlanes ?? []; + const inputSwimlanes = outputSwimlanesFromPreviousItem.map(i => deepClone(i)); + const outputSwimlanes: ISCMHistoryItemGraphNode[] = []; + + if (historyItem.parentIds.length > 0) { + let firstParentAdded = false; + + // Add first parent to the output + for (const node of inputSwimlanes) { + if (node.id === historyItem.id) { + if (!firstParentAdded) { + outputSwimlanes.push({ + id: historyItem.parentIds[0], + color: getLabelColorIndex(historyItem, colorMap) ?? node.color + }); + firstParentAdded = true; + } + + continue; + } + + outputSwimlanes.push(deepClone(node)); + } + + // Add unprocessed parent(s) to the output + for (let i = firstParentAdded ? 1 : 0; i < historyItem.parentIds.length; i++) { + // Color index (label -> next color) + colorIndex = getLabelColorIndex(historyItem, colorMap) ?? getNextColorIndex(colorIndex); + + outputSwimlanes.push({ + id: historyItem.parentIds[i], + color: colorIndex + }); + } + } + + viewModels.push({ + historyItem, + inputSwimlanes, + outputSwimlanes, + }); + } + + return viewModels; +} diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts index 890e39b459f..99e9f61f835 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -18,7 +18,6 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RepositoryActionRunner, RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer'; @@ -82,9 +81,7 @@ export class SCMRepositoriesViewPane extends ViewPane { this.list = this.instantiationService.createInstance(WorkbenchList, `SCM Main`, listContainer, delegate, [renderer], { identityProvider, horizontalScrolling: false, - overrideStyles: { - listBackground: SIDE_BAR_BACKGROUND - }, + overrideStyles: this.getLocationBasedColors().listOverrideStyles, accessibilityProvider: { getAriaLabel(r: ISCMRepository) { return r.provider.label; diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts index e33a86c854a..594d24ef8f3 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts @@ -4,14 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/scm'; -import { IDisposable, DisposableStore, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; +import { autorun } from 'vs/base/common/observable'; import { append, $ } from 'vs/base/browser/dom'; import { ISCMProvider, ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ActionRunner, IAction } from 'vs/base/common/actions'; -import { connectPrimaryMenu, isSCMRepository, StatusBarAction } from './util'; +import { connectPrimaryMenu, getRepositoryResourceCount, isSCMRepository, StatusBarAction } from './util'; import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { FuzzyScore } from 'vs/base/common/filters'; @@ -23,6 +24,9 @@ import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IManagedHover } from 'vs/base/browser/ui/hover/hover'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export class RepositoryActionRunner extends ActionRunner { constructor(private readonly getSelectedRepositories: () => ISCMRepository[]) { @@ -43,6 +47,7 @@ export class RepositoryActionRunner extends ActionRunner { interface RepositoryTemplate { readonly label: HTMLElement; + readonly labelCustomHover: IManagedHover; readonly name: HTMLElement; readonly description: HTMLElement; readonly countContainer: HTMLElement; @@ -64,6 +69,7 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer provider.classList.toggle('active', e)); - const templateDisposable = combinedDisposable(visibilityDisposable, toolBar); + const templateDisposable = combinedDisposable(labelCustomHover, visibilityDisposable, toolBar); - return { label, name, description, countContainer, count, toolBar, elementDisposables: new DisposableStore(), templateDisposable }; + return { label, labelCustomHover, name, description, countContainer, count, toolBar, elementDisposables: new DisposableStore(), templateDisposable }; } renderElement(arg: ISCMRepository | ITreeNode, index: number, templateData: RepositoryTemplate, height: number | undefined): void { @@ -95,10 +102,10 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer { - const commands = repository.provider.statusBarCommands || []; + templateData.elementDisposables.add(autorun(reader => { + const commands = repository.provider.statusBarCommands.read(reader) ?? []; statusPrimaryActions = commands.map(c => new StatusBarAction(c, this.commandService)); updateToolbar(); - - const count = repository.provider.count || 0; - templateData.countContainer.setAttribute('data-count', String(count)); - templateData.count.setCount(count); - }; - - // TODO@joao TODO@lszomoru - let disposed = false; - templateData.elementDisposables.add(toDisposable(() => disposed = true)); - templateData.elementDisposables.add(repository.provider.onDidChange(() => { - if (disposed) { - return; - } - - onDidChangeProvider(); })); - onDidChangeProvider(); + templateData.elementDisposables.add(autorun(reader => { + const count = repository.provider.count.read(reader) ?? getRepositoryResourceCount(repository.provider); + templateData.countContainer.setAttribute('data-count', String(count)); + templateData.count.setCount(count); + })); const repositoryMenus = this.scmViewService.menus.getRepositoryMenus(repository.provider); const menu = this.toolbarMenuId === MenuId.SCMTitle ? repositoryMenus.titleMenu.menu : repositoryMenus.repositoryMenu; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 185aa10316f..67564cd4aab 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -8,9 +8,9 @@ import { Event, Emitter } from 'vs/base/common/event'; import { basename, dirname } from 'vs/base/common/resources'; import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, DisposableMap } from 'vs/base/common/lifecycle'; import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; -import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend, isPointerEvent } from 'vs/base/browser/dom'; +import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend, isPointerEvent, isActiveElement } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemViewModel, SCMHistoryItemViewModelTreeElement, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, ISCMInputValueProviderContext, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -23,8 +23,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, MenuRegistry, Action2, IMenu } from 'vs/platform/actions/common/actions'; import { IAction, ActionRunner, Action, Separator, IActionRunner } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator, connectPrimaryMenu } from './util'; +import { IThemeService, IFileIconTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator, connectPrimaryMenu, isSCMHistoryItemViewModelTreeElement } from './util'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, Sequencer, ThrottledDelayer, Throttler } from 'vs/base/common/async'; @@ -38,10 +38,9 @@ import { FileKind } from 'vs/platform/files/common/files'; import { compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { localize, localize2 } from 'vs/nls'; +import { localize } from 'vs/nls'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; -import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; @@ -96,20 +95,25 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { EditOperation } from 'vs/editor/common/core/editOperation'; import { stripIcons } from 'vs/base/common/iconLabels'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { foreground, listActiveSelectionForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { editorSelectionBackground, foreground, inputBackground, inputForeground, listActiveSelectionForeground, registerColor, selectionBackground, transparent } from 'vs/platform/theme/common/colorRegistry'; import { IMenuWorkbenchToolBarOptions, MenuWorkbenchToolBar, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; -import { clamp } from 'vs/base/common/numbers'; +import { clamp, rot } from 'vs/base/common/numbers'; import { ILogService } from 'vs/platform/log/common/log'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import type { IUpdatableHover, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; +import type { IHoverOptions, IManagedHover, IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import { ITextModel } from 'vs/editor/common/model'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { autorun } from 'vs/base/common/observable'; +import { createInstantHoverDelegate, getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory'; +import { PlaceholderTextContribution } from 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; // type SCMResourceTreeNode = IResourceNode; // type SCMHistoryItemChangeResourceTreeNode = IResourceNode; @@ -122,39 +126,20 @@ type TreeElement = IResourceNode | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | + SCMHistoryItemViewModelTreeElement | SCMHistoryItemChangeTreeElement | IResourceNode | SCMViewSeparatorElement; type ShowChangesSetting = 'always' | 'never' | 'auto'; -registerColor('scm.historyItemAdditionsForeground', { - dark: 'gitDecoration.addedResourceForeground', - light: 'gitDecoration.addedResourceForeground', - hcDark: 'gitDecoration.addedResourceForeground', - hcLight: 'gitDecoration.addedResourceForeground' -}, localize('scm.historyItemAdditionsForeground', "History item additions foreground color.")); +const historyItemAdditionsForeground = registerColor('scm.historyItemAdditionsForeground', 'gitDecoration.addedResourceForeground', localize('scm.historyItemAdditionsForeground', "History item additions foreground color.")); -registerColor('scm.historyItemDeletionsForeground', { - dark: 'gitDecoration.deletedResourceForeground', - light: 'gitDecoration.deletedResourceForeground', - hcDark: 'gitDecoration.deletedResourceForeground', - hcLight: 'gitDecoration.deletedResourceForeground' -}, localize('scm.historyItemDeletionsForeground', "History item deletions foreground color.")); +const historyItemDeletionsForeground = registerColor('scm.historyItemDeletionsForeground', 'gitDecoration.deletedResourceForeground', localize('scm.historyItemDeletionsForeground', "History item deletions foreground color.")); -registerColor('scm.historyItemStatisticsBorder', { - dark: transparent(foreground, 0.2), - light: transparent(foreground, 0.2), - hcDark: transparent(foreground, 0.2), - hcLight: transparent(foreground, 0.2) -}, localize('scm.historyItemStatisticsBorder', "History item statistics border color.")); +registerColor('scm.historyItemStatisticsBorder', transparent(foreground, 0.2), localize('scm.historyItemStatisticsBorder', "History item statistics border color.")); -registerColor('scm.historyItemSelectedStatisticsBorder', { - dark: transparent(listActiveSelectionForeground, 0.2), - light: transparent(listActiveSelectionForeground, 0.2), - hcDark: transparent(listActiveSelectionForeground, 0.2), - hcLight: transparent(listActiveSelectionForeground, 0.2) -}, localize('scm.historyItemSelectedStatisticsBorder', "History item selected statistics border color.")); +registerColor('scm.historyItemSelectedStatisticsBorder', transparent(listActiveSelectionForeground, 0.2), localize('scm.historyItemSelectedStatisticsBorder', "History item selected statistics border color.")); function processResourceFilterData(uri: URI, filterData: FuzzyScore | LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { if (!filterData) { @@ -858,11 +843,36 @@ class HistoryItemActionRunner extends ActionRunner { } } +class HistoryItemHoverDelegate extends WorkbenchHoverDelegate { + constructor( + private readonly viewContainerLocation: ViewContainerLocation | null, + private readonly sideBarPosition: Position, + @IConfigurationService configurationService: IConfigurationService, + @IHoverService hoverService: IHoverService + + ) { + super('element', true, () => this.getHoverOptions(), configurationService, hoverService); + } + + private getHoverOptions(): Partial { + let hoverPosition: HoverPosition; + if (this.viewContainerLocation === ViewContainerLocation.Sidebar) { + hoverPosition = this.sideBarPosition === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; + } else if (this.viewContainerLocation === ViewContainerLocation.AuxiliaryBar) { + hoverPosition = this.sideBarPosition === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; + } else { + hoverPosition = HoverPosition.RIGHT; + } + + return { position: { hoverPosition, forcePosition: true } }; + } +} + interface HistoryItemTemplate { readonly iconContainer: HTMLElement; readonly label: IconLabel; readonly statsContainer: HTMLElement; - readonly statsCustomHover: IUpdatableHover; + readonly statsCustomHover: IManagedHover; readonly filesLabel: HTMLElement; readonly insertionsLabel: HTMLElement; readonly deletionsLabel: HTMLElement; @@ -902,7 +912,7 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer { + + static readonly TEMPLATE_ID = 'history-item-2'; + get templateId(): string { return HistoryItem2Renderer.TEMPLATE_ID; } + + constructor( + private readonly hoverDelegate: IHoverDelegate, + @IHoverService private readonly hoverService: IHoverService, + @IThemeService private readonly themeService: IThemeService + ) { } + + renderTemplate(container: HTMLElement): HistoryItem2Template { + // hack + (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-no-twistie'); + + const element = append(container, $('.history-item')); + const graphContainer = append(element, $('.graph-container')); + const iconLabel = new IconLabel(element, { supportIcons: true, supportHighlights: true, supportDescriptionHighlights: true }); + + const labelContainer = append(element, $('.label-container')); + element.appendChild(labelContainer); + + return { element, graphContainer, label: iconLabel, labelContainer, elementDisposables: new DisposableStore(), disposables: new DisposableStore() }; + } + + renderElement(node: ITreeNode, index: number, templateData: HistoryItem2Template, height: number | undefined): void { + const historyItemViewModel = node.element.historyItemViewModel; + const historyItem = historyItemViewModel.historyItem; + + const historyItemHover = this.hoverService.setupManagedHover(this.hoverDelegate, templateData.element, this.getTooltip(historyItemViewModel)); + templateData.elementDisposables.add(historyItemHover); + + templateData.graphContainer.textContent = ''; + templateData.graphContainer.appendChild(renderSCMHistoryItemGraph(historyItemViewModel)); + + const [matches, descriptionMatches] = this.processMatches(historyItemViewModel, node.filterData); + templateData.label.setLabel(historyItem.message, historyItem.author, { matches, descriptionMatches }); + + templateData.labelContainer.textContent = ''; + if (historyItem.labels) { + const instantHoverDelegate = createInstantHoverDelegate(); + templateData.elementDisposables.add(instantHoverDelegate); + + for (const label of historyItem.labels) { + if (label.icon && ThemeIcon.isThemeIcon(label.icon)) { + const icon = append(templateData.labelContainer, $('div.label')); + icon.classList.add(...ThemeIcon.asClassNameArray(label.icon)); + + const hover = this.hoverService.setupManagedHover(instantHoverDelegate, icon, label.title); + templateData.elementDisposables.add(hover); + } + } + } + } + + renderCompressedElements(node: ITreeNode, LabelFuzzyScore>, index: number, templateData: HistoryItem2Template, height: number | undefined): void { + throw new Error('Should never happen since node is incompressible'); + } + + private getTooltip(historyItemViewModel: ISCMHistoryItemViewModel): IManagedHoverTooltipMarkdownString { + const historyItem = historyItemViewModel.historyItem; + const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); + + if (historyItem.author) { + markdown.appendMarkdown(`$(account) **${historyItem.author}**\n\n`); + } + + markdown.appendMarkdown(`${historyItem.message}\n\n`); + + if (historyItem.timestamp) { + markdown.appendMarkdown(`---\n\n`); + + const dateFormatter = new Intl.DateTimeFormat(platform.language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); + markdown.appendMarkdown(`$(history) ${dateFormatter.format(historyItem.timestamp)}`); + } + + if (historyItem.statistics?.files) { + const colorTheme = this.themeService.getColorTheme(); + const historyItemAdditionsForegroundColor = colorTheme.getColor(historyItemAdditionsForeground); + const historyItemDeletionsForegroundColor = colorTheme.getColor(historyItemDeletionsForeground); + + markdown.appendMarkdown(` | `); + markdown.appendMarkdown(`${historyItem.statistics.files}`); + markdown.appendMarkdown(historyItem.statistics.insertions ? `, +${historyItem.statistics.insertions}` : ''); + markdown.appendMarkdown(historyItem.statistics.deletions ? `, -${historyItem.statistics.deletions}` : ''); + } + + return { markdown, markdownNotSupportedFallback: historyItem.message }; + } + + private processMatches(historyItemViewModel: ISCMHistoryItemViewModel, filterData: LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { + if (!filterData) { + return [undefined, undefined]; + } + + return [ + historyItemViewModel.historyItem.message === filterData.label ? createMatches(filterData.score) : undefined, + historyItemViewModel.historyItem.author === filterData.label ? createMatches(filterData.score) : undefined + ]; + } + + disposeElement(element: ITreeNode, index: number, templateData: HistoryItem2Template, height: number | undefined): void { + templateData.elementDisposables.clear(); + } + + disposeTemplate(templateData: HistoryItem2Template): void { + templateData.disposables.dispose(); + } +} + interface HistoryItemChangeTemplate { readonly element: HTMLElement; readonly name: HTMLElement; @@ -1084,6 +1213,7 @@ class SeparatorRenderer implements ICompressibleTreeRenderer('scm.experimental.showHistoryGraph') !== true) { + const toolBar = new MenuWorkbenchToolBar(append(element, $('.actions')), MenuId.SCMChangesSeparator, { moreIcon: Codicon.gear }, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService); + disposables.add(toolBar); + } return { label, disposables }; } @@ -1150,6 +1282,8 @@ class ListDelegate implements IListVirtualDelegate { return HistoryItemGroupRenderer.TEMPLATE_ID; } else if (isSCMHistoryItemTreeElement(element)) { return HistoryItemRenderer.TEMPLATE_ID; + } else if (isSCMHistoryItemViewModelTreeElement(element)) { + return HistoryItem2Renderer.TEMPLATE_ID; } else if (isSCMHistoryItemChangeTreeElement(element) || isSCMHistoryItemChangeNode(element)) { return HistoryItemChangeRenderer.TEMPLATE_ID; } else if (isSCMViewSeparator(element)) { @@ -1230,6 +1364,10 @@ export class SCMTreeSorter implements ITreeSorter { return 0; } + if (isSCMHistoryItemViewModelTreeElement(one)) { + return isSCMHistoryItemViewModelTreeElement(other) ? 0 : 1; + } + if (isSCMHistoryItemChangeTreeElement(one) || isSCMHistoryItemChangeNode(one)) { // List if (this.viewMode() === ViewMode.List) { @@ -1314,6 +1452,11 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb // the author. A match in the message takes precedence over // a match in the author. return [element.message, element.author]; + } else if (isSCMHistoryItemViewModelTreeElement(element)) { + // For a history item we want to match both the message and + // the author. A match in the message takes precedence over + // a match in the author. + return [element.historyItemViewModel.historyItem.message, element.historyItemViewModel.historyItem.author]; } else if (isSCMViewSeparator(element)) { return element.label; } else { @@ -1364,6 +1507,10 @@ function getSCMResourceId(element: TreeElement): string { const historyItemGroup = element.historyItemGroup; const provider = historyItemGroup.repository.provider; return `historyItem:${provider.id}/${historyItemGroup.id}/${element.id}/${element.parentIds.join(',')}`; + } else if (isSCMHistoryItemViewModelTreeElement(element)) { + const provider = element.repository.provider; + const historyItem = element.historyItemViewModel.historyItem; + return `historyItem2:${provider.id}/${historyItem.id}/${historyItem.parentIds.join(',')}`; } else if (isSCMHistoryItemChangeTreeElement(element)) { const historyItem = element.historyItem; const historyItemGroup = historyItem.historyItemGroup; @@ -1414,6 +1561,9 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider('scmViewMode', ViewMode.List), SCMViewSortKey: new RawContextKey('scmViewSortKey', ViewSortKey.Path), SCMViewAreAllRepositoriesCollapsed: new RawContextKey('scmViewAreAllRepositoriesCollapsed', false), @@ -1488,7 +1638,7 @@ MenuRegistry.appendMenuItem(MenuId.SCMTitle, { MenuRegistry.appendMenuItem(MenuId.SCMTitle, { title: localize('scmChanges', "Incoming & Outgoing"), submenu: Menus.ChangesSettings, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeyExpr.equals('config.scm.experimental.showHistoryGraph', true).negate()), group: '0_view&sort', order: 2 }); @@ -1958,26 +2108,6 @@ class ExpandAllRepositoriesAction extends ViewAction { registerAction2(CollapseAllRepositoriesAction); registerAction2(ExpandAllRepositoriesAction); -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'workbench.scm.action.focusInput', - title: { ...localize2('focusInput', "Focus Input") }, - category: localize2('source control', "Source Control"), - precondition: ContextKeys.RepositoryCount.notEqualsTo(0), - f1: true - }); - } - - override async run(accessor: ServicesAccessor) { - const viewsService = accessor.get(IViewsService); - const scmView = await viewsService.openView(VIEW_PANE_ID); - if (scmView) { - scmView.focusInput(); - } - } -}); - const enum SCMInputWidgetCommandId { CancelAction = 'scm.input.cancelAction' } @@ -2049,7 +2179,7 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar { private _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; - private readonly repositoryDisposables = new DisposableStore(); + private readonly _disposables = this._register(new MutableDisposable()); constructor( container: HTMLElement, @@ -2077,7 +2207,7 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar { } public setInput(input: ISCMInput): void { - this.repositoryDisposables.clear(); + this._disposables.value = new DisposableStore(); const contextKeyService = this.contextKeyService.createOverlay([ ['scmProvider', input.repository.provider.contextValue], @@ -2085,7 +2215,7 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar { ['scmProviderHasRootUri', !!input.repository.provider.rootUri] ]); - const menu = this.repositoryDisposables.add(this.menuService.createMenu(MenuId.SCMInputBox, contextKeyService, { emitEventsForSubmenuChanges: true })); + const menu = this._disposables.value.add(this.menuService.createMenu(MenuId.SCMInputBox, contextKeyService, { emitEventsForSubmenuChanges: true })); const isEnabled = (): boolean => { return input.repository.provider.groups.some(g => g.resources.length > 0); @@ -2115,18 +2245,18 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar { this._onDidChange.fire(); }; - this.repositoryDisposables.add(menu.onDidChange(() => updateToolbar())); - this.repositoryDisposables.add(input.repository.provider.onDidChangeResources(() => updateToolbar())); - this.repositoryDisposables.add(this.storageService.onDidChangeValue(StorageScope.PROFILE, SCMInputWidgetStorageKey.LastActionId, this.repositoryDisposables)(() => updateToolbar())); + this._disposables.value.add(menu.onDidChange(() => updateToolbar())); + this._disposables.value.add(input.repository.provider.onDidChangeResources(() => updateToolbar())); + this._disposables.value.add(this.storageService.onDidChangeValue(StorageScope.PROFILE, SCMInputWidgetStorageKey.LastActionId, this._disposables.value)(() => updateToolbar())); this.actionRunner = new SCMInputWidgetActionRunner(input, this.storageService); - this.repositoryDisposables.add(this.actionRunner.onWillRun(e => { + this._disposables.value.add(this.actionRunner.onWillRun(e => { if ((this.actionRunner as SCMInputWidgetActionRunner).runningActions.size === 0) { super.setActions([this._cancelAction], []); this._onDidChange.fire(); } })); - this.repositoryDisposables.add(this.actionRunner.onDidRun(e => { + this._disposables.value.add(this.actionRunner.onDidRun(e => { if ((this.actionRunner as SCMInputWidgetActionRunner).runningActions.size === 0) { updateToolbar(); } @@ -2134,7 +2264,6 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar { updateToolbar(); } - } class SCMInputWidgetEditorOptions { @@ -2258,7 +2387,6 @@ class SCMInputWidget { private element: HTMLElement; private editorContainer: HTMLElement; - private placeholderTextContainer: HTMLElement; private readonly inputEditor: CodeEditorWidget; private readonly inputEditorOptions: SCMInputWidgetEditorOptions; private toolbarContainer: HTMLElement; @@ -2349,14 +2477,11 @@ class SCMInputWidget { this.repositoryDisposables.add(input.onDidChangeValidationMessage((e) => this.setValidation(e, { focus: true, timeout: true }))); this.repositoryDisposables.add(input.onDidChangeValidateInput((e) => triggerValidation())); - // Keep API in sync with model, update placeholder visibility and validate - const updatePlaceholderVisibility = () => this.placeholderTextContainer.classList.toggle('hidden', textModel.getValueLength() > 0); + // Keep API in sync with model and validate this.repositoryDisposables.add(textModel.onDidChangeContent(() => { input.setValue(textModel.getValue(), true); - updatePlaceholderVisibility(); triggerValidation(); })); - updatePlaceholderVisibility(); // Update placeholder text const updatePlaceholderText = () => { @@ -2364,8 +2489,7 @@ class SCMInputWidget { const label = binding ? binding.getLabel() : (platform.isMacintosh ? 'Cmd+Enter' : 'Ctrl+Enter'); const placeholderText = format(input.placeholder, label); - this.inputEditor.updateOptions({ ariaLabel: placeholderText }); - this.placeholderTextContainer.textContent = placeholderText; + this.inputEditor.updateOptions({ placeholder: placeholderText }); }; this.repositoryDisposables.add(input.onDidChangePlaceholder(updatePlaceholderText)); this.repositoryDisposables.add(this.keybindingService.onDidUpdateKeybindings(updatePlaceholderText)); @@ -2373,24 +2497,21 @@ class SCMInputWidget { // Update input template let commitTemplate = ''; - const updateTemplate = () => { - if (typeof input.repository.provider.commitTemplate === 'undefined' || !input.visible) { + this.repositoryDisposables.add(autorun(reader => { + if (!input.visible) { return; } const oldCommitTemplate = commitTemplate; - commitTemplate = input.repository.provider.commitTemplate; + commitTemplate = input.repository.provider.commitTemplate.read(reader); const value = textModel.getValue(); - if (value && value !== oldCommitTemplate) { return; } textModel.setValue(commitTemplate); - }; - this.repositoryDisposables.add(input.repository.provider.onDidChangeCommitTemplate(updateTemplate, this)); - updateTemplate(); + })); // Update input enablement const updateEnablement = (enabled: boolean) => { @@ -2449,7 +2570,6 @@ class SCMInputWidget { ) { this.element = append(container, $('.scm-editor')); this.editorContainer = append(this.element, $('.scm-editor-container')); - this.placeholderTextContainer = append(this.editorContainer, $('.scm-editor-placeholder')); this.toolbarContainer = append(this.element, $('.scm-editor-toolbar')); this.contextKeyService = contextKeyService.createScoped(this.element); @@ -2459,33 +2579,32 @@ class SCMInputWidget { this.disposables.add(this.inputEditorOptions.onDidChange(this.onDidChangeEditorOptions, this)); this.disposables.add(this.inputEditorOptions); - const editorConstructionOptions = this.inputEditorOptions.getEditorConstructionOptions(); - this.setPlaceholderFontStyles(editorConstructionOptions.fontFamily!, editorConstructionOptions.fontSize!, editorConstructionOptions.lineHeight!); - const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - isSimpleWidget: true, contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + CodeActionController.ID, ColorDetector.ID, ContextMenuController.ID, - DragAndDropController.ID, CopyPasteController.ID, + DragAndDropController.ID, DropIntoEditorController.ID, + EditorDictation.ID, + FormatOnType.ID, + HoverController.ID, + InlineCompletionsController.ID, LinkDetector.ID, MenuPreventer.ID, MessageController.ID, - HoverController.ID, + PlaceholderTextContribution.ID, SelectionClipboardContributionID, SnippetController2.ID, - SuggestController.ID, - InlineCompletionsController.ID, - CodeActionController.ID, - FormatOnType.ID, - EditorDictation.ID, - ]) + SuggestController.ID + ]), + isSimpleWidget: true }; const services = new ServiceCollection([IContextKeyService, this.contextKeyService]); - const instantiationService2 = instantiationService.createChild(services); + const instantiationService2 = instantiationService.createChild(services, this.disposables); + const editorConstructionOptions = this.inputEditorOptions.getEditorConstructionOptions(); this.inputEditor = instantiationService2.createInstance(CodeEditorWidget, this.editorContainer, editorConstructionOptions, codeEditorWidgetOptions); this.disposables.add(this.inputEditor); @@ -2575,7 +2694,6 @@ class SCMInputWidget { this.lastLayoutWasTrash = false; this.inputEditor.layout(dimension); - this.placeholderTextContainer.style.width = `${dimension.width}px`; this.renderValidation(); const showInputActionButton = this.configurationService.getValue('scm.showInputActionButton') === true; @@ -2603,10 +2721,7 @@ class SCMInputWidget { } private onDidChangeEditorOptions(): void { - const editorOptions = this.inputEditorOptions.getEditorOptions(); - - this.inputEditor.updateOptions(editorOptions); - this.setPlaceholderFontStyles(editorOptions.fontFamily!, editorOptions.fontSize!, editorOptions.lineHeight!); + this.inputEditor.updateOptions(this.inputEditorOptions.getEditorOptions()); } private renderValidation(): void { @@ -2696,11 +2811,6 @@ class SCMInputWidget { 26 /* 22px action + 4px margin */ : 39 /* 35px action + 4px margin */; } - private setPlaceholderFontStyles(fontFamily: string, fontSize: number, lineHeight: number): void { - this.placeholderTextContainer.style.fontFamily = fontFamily; - this.placeholderTextContainer.style.fontSize = `${fontSize}px`; - this.placeholderTextContainer.style.lineHeight = `${lineHeight}px`; - } clearValidation(): void { this.validationContextView?.close(); @@ -2802,6 +2912,7 @@ export class SCMViewPane extends ViewPane { @ISCMViewService private readonly scmViewService: ISCMViewService, @IStorageService private readonly storageService: IStorageService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IKeybindingService keybindingService: IKeybindingService, @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, @@ -2918,7 +3029,8 @@ export class SCMViewPane extends ViewPane { e.affectsConfiguration('scm.showActionButton') || e.affectsConfiguration('scm.showChangesSummary') || e.affectsConfiguration('scm.showIncomingChanges') || - e.affectsConfiguration('scm.showOutgoingChanges'), + e.affectsConfiguration('scm.showOutgoingChanges') || + e.affectsConfiguration('scm.experimental.showHistoryGraph'), this.visibilityDisposables) (() => this.updateChildren(), this, this.visibilityDisposables); @@ -2979,6 +3091,9 @@ export class SCMViewPane extends ViewPane { historyItemActionRunner.onWillRun(() => this.tree.domFocus(), this, this.disposables); this.disposables.add(historyItemActionRunner); + const historyItemHoverDelegate = this.instantiationService.createInstance(HistoryItemHoverDelegate, this.viewDescriptorService.getViewLocationById(this.id), this.layoutService.getSideBarPosition()); + this.disposables.add(historyItemHoverDelegate); + const treeDataSource = this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode); this.disposables.add(treeDataSource); @@ -2996,6 +3111,7 @@ export class SCMViewPane extends ViewPane { this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), resourceActionRunner), this.instantiationService.createInstance(HistoryItemGroupRenderer, historyItemGroupActionRunner), this.instantiationService.createInstance(HistoryItemRenderer, historyItemActionRunner, getActionViewItemProvider(this.instantiationService)), + this.instantiationService.createInstance(HistoryItem2Renderer, historyItemHoverDelegate), this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewMode, this.listLabels), this.instantiationService.createInstance(SeparatorRenderer) ], @@ -3009,9 +3125,7 @@ export class SCMViewPane extends ViewPane { identityProvider: new SCMResourceIdentityProvider(), sorter: new SCMTreeSorter(() => this.viewMode, () => this.viewSortKey), keyboardNavigationLabelProvider: this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider, () => this.viewMode), - overrideStyles: { - listBackground: this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND - }, + overrideStyles: this.getLocationBasedColors().listOverrideStyles, collapseByDefault: (e: unknown) => { // Repository, Resource Group, Resource Folder (Tree), History Item Change Folder (Tree) if (isSCMRepository(e) || isSCMResourceGroup(e) || isSCMResourceNode(e) || isSCMHistoryItemChangeNode(e)) { @@ -3121,6 +3235,25 @@ export class SCMViewPane extends ViewPane { } else if (isSCMHistoryItemTreeElement(e.element)) { this.scmViewService.focus(e.element.historyItemGroup.repository); return; + } else if (isSCMHistoryItemViewModelTreeElement(e.element)) { + const historyItem = e.element.historyItemViewModel.historyItem; + const historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : undefined; + + const historyProvider = e.element.repository.provider.historyProvider; + const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItem.id, historyItemParentId); + if (historyItemChanges) { + const title = `${historyItem.id.substring(0, 8)} - ${historyItem.message}`; + + const rootUri = e.element.repository.provider.rootUri; + const multiDiffSourceUri = rootUri ? + rootUri.with({ scheme: 'scm-history-item', path: `${rootUri.path}/${historyItem.id}` }) : + { scheme: 'scm-history-item', path: `${e.element.repository.provider.label}/${historyItem.id}` }; + + await this.commandService.executeCommand('_workbench.openMultiDiffEditor', { title, multiDiffSourceUri, resources: historyItemChanges }); + } + + this.scmViewService.focus(e.element.repository); + return; } else if (isSCMHistoryItemChangeTreeElement(e.element)) { if (e.element.originalUri && e.element.modifiedUri) { await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.uri, e.element.originalUri, e.element.modifiedUri), e); @@ -3170,6 +3303,8 @@ export class SCMViewPane extends ViewPane { if (resource) { await this.tree.expandTo(resource); + this.tree.reveal(resource); + this.tree.setSelection([resource]); this.tree.setFocus([resource]); return; @@ -3239,16 +3374,14 @@ export class SCMViewPane extends ViewPane { private onListContextMenu(e: ITreeContextMenuEvent): void { if (!e.element) { - const menu = this.menuService.createMenu(Menus.ViewSort, this.contextKeyService); + const menu = this.menuService.getMenuActions(Menus.ViewSort, this.contextKeyService); const actions: IAction[] = []; - createAndFillInContextMenuActions(menu, undefined, actions); + createAndFillInContextMenuActions(menu, actions); return this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => actions, - onHide: () => { - menu.dispose(); - } + onHide: () => { } }); } @@ -3433,8 +3566,8 @@ export class SCMViewPane extends ViewPane { return; } - this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r))); - this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); + this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasNode(r) && this.tree.isCollapsible(r))); + this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasNode(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); } collapseAllRepositories(): void { @@ -3453,17 +3586,90 @@ export class SCMViewPane extends ViewPane { } } - focusInput(): void { - this.treeOperationSequencer.queue(() => { - return new Promise(resolve => { - if (this.scmViewService.focusedRepository) { - this.tree.reveal(this.scmViewService.focusedRepository.input, 0.5); - this.inputRenderer.getRenderedInputWidget(this.scmViewService.focusedRepository.input)?.focus(); - } + focusPreviousInput(): void { + this.treeOperationSequencer.queue(() => this.focusInput(-1)); + } - resolve(); - }); - }); + focusNextInput(): void { + this.treeOperationSequencer.queue(() => this.focusInput(1)); + } + + private async focusInput(delta: number): Promise { + if (!this.scmViewService.focusedRepository || + this.scmViewService.visibleRepositories.length === 0) { + return; + } + + let input = this.scmViewService.focusedRepository.input; + const repositories = this.scmViewService.visibleRepositories; + + // One visible repository and the input is already focused + if (repositories.length === 1 && this.inputRenderer.getRenderedInputWidget(input)?.hasFocus() === true) { + return; + } + + // Multiple visible repositories and the input already focused + if (repositories.length > 1 && this.inputRenderer.getRenderedInputWidget(input)?.hasFocus() === true) { + const focusedRepositoryIndex = repositories.indexOf(this.scmViewService.focusedRepository); + const newFocusedRepositoryIndex = rot(focusedRepositoryIndex + delta, repositories.length); + input = repositories[newFocusedRepositoryIndex].input; + } + + await this.tree.expandTo(input); + + this.tree.reveal(input); + this.inputRenderer.getRenderedInputWidget(input)?.focus(); + } + + focusPreviousResourceGroup(): void { + this.treeOperationSequencer.queue(() => this.focusResourceGroup(-1)); + } + + focusNextResourceGroup(): void { + this.treeOperationSequencer.queue(() => this.focusResourceGroup(1)); + } + + private async focusResourceGroup(delta: number): Promise { + if (!this.scmViewService.focusedRepository || + this.scmViewService.visibleRepositories.length === 0) { + return; + } + + const treeHasDomFocus = isActiveElement(this.tree.getHTMLElement()); + const resourceGroups = this.scmViewService.focusedRepository.provider.groups; + const focusedResourceGroup = this.tree.getFocus().find(e => isSCMResourceGroup(e)); + const focusedResourceGroupIndex = treeHasDomFocus && focusedResourceGroup ? resourceGroups.indexOf(focusedResourceGroup) : -1; + + let resourceGroupNext: ISCMResourceGroup | undefined; + + if (focusedResourceGroupIndex === -1) { + // First visible resource group + for (const resourceGroup of resourceGroups) { + if (this.tree.hasNode(resourceGroup)) { + resourceGroupNext = resourceGroup; + break; + } + } + } else { + // Next/Previous visible resource group + let index = rot(focusedResourceGroupIndex + delta, resourceGroups.length); + while (index !== focusedResourceGroupIndex) { + if (this.tree.hasNode(resourceGroups[index])) { + resourceGroupNext = resourceGroups[index]; + break; + } + index = rot(index + delta, resourceGroups.length); + } + } + + if (resourceGroupNext) { + await this.tree.expandTo(resourceGroupNext); + this.tree.reveal(resourceGroupNext); + + this.tree.setSelection([resourceGroupNext]); + this.tree.setFocus([resourceGroupNext]); + this.tree.domFocus(); + } } override shouldShowWelcome(): boolean { @@ -3549,6 +3755,8 @@ class SCMTreeDataSource implements IAsyncDataSource 0) { + const label = localize('syncSeparatorHeader', "Incoming/Outgoing"); + const ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); + + children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); + } + + children.push(...historyItems); + return children; } else if (isSCMResourceGroup(inputOrElement)) { if (this.viewMode() === ViewMode.List) { @@ -3649,13 +3868,13 @@ class SCMTreeDataSource implements IAsyncDataSource { - const { showIncomingChanges, showOutgoingChanges } = this.getConfiguration(); + const { showIncomingChanges, showOutgoingChanges, showHistoryGraph } = this.getConfiguration(); const scmProvider = element.provider; const historyProvider = scmProvider.historyProvider; const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never')) { + if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never') || showHistoryGraph) { return []; } @@ -3667,16 +3886,16 @@ class SCMTreeDataSource implements IAsyncDataSource { + const { showHistoryGraph } = this.getConfiguration(); + + const historyProvider = element.provider.historyProvider; + const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; + + if (!currentHistoryItemGroup || !showHistoryGraph) { + return []; + } + + const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(element); + let historyItemsElement = historyProviderCacheEntry.historyItems2.get(element.id); + const historyItemsMap = historyProviderCacheEntry.historyItems2; + + if (!historyItemsElement) { + const historyItemGroupIds = [ + currentHistoryItemGroup.id, + ...currentHistoryItemGroup.remote ? [currentHistoryItemGroup.remote.id] : [], + ...currentHistoryItemGroup.base ? [currentHistoryItemGroup.base.id] : [], + ]; + + historyItemsElement = await historyProvider.provideHistoryItems2({ historyItemGroupIds }) ?? []; + + this.historyProviderCache.set(element, { + ...historyProviderCacheEntry, + historyItems2: historyItemsMap.set(element.id, historyItemsElement) + }); + } + + // If we only have one history item that matches + // the current history item group, don't show it + if (historyItemsElement.length === 1 && + historyItemsElement[0].labels?.find(l => l.title === currentHistoryItemGroup.name)) { + return []; + } + + // Create the color map + // TODO@lszomoru - use theme colors + const colorMap = new Map([ + [currentHistoryItemGroup.name, 0] + ]); + if (currentHistoryItemGroup.remote) { + colorMap.set(currentHistoryItemGroup.remote.name, 1); + } + if (currentHistoryItemGroup.base) { + colorMap.set(currentHistoryItemGroup.base.name, 2); + } + + return toISCMHistoryItemViewModelArray(historyItemsElement, colorMap) + .map(historyItemViewModel => ({ + repository: element, + historyItemViewModel, + type: 'historyItem2' + }) satisfies SCMHistoryItemViewModelTreeElement); + } + private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode)[]> { const repository = element.historyItemGroup.repository; const historyProvider = repository.provider.historyProvider; @@ -3847,6 +4122,15 @@ class SCMTreeDataSource implements IAsyncDataSource r.provider === element.provider); + if (!repository) { + throw new Error('Invalid element passed to getParent'); + } + + return repository; } else { throw new Error('Unexpected call to getParent'); } @@ -3858,13 +4142,15 @@ class SCMTreeDataSource implements IAsyncDataSource('scm.alwaysShowRepositories'), showActionButton: this.configurationService.getValue('scm.showActionButton'), showChangesSummary: this.configurationService.getValue('scm.showChangesSummary'), showIncomingChanges: this.configurationService.getValue('scm.showIncomingChanges'), - showOutgoingChanges: this.configurationService.getValue('scm.showOutgoingChanges') + showOutgoingChanges: this.configurationService.getValue('scm.showOutgoingChanges'), + showHistoryGraph: this.configurationService.getValue('scm.experimental.showHistoryGraph') }; } @@ -3902,6 +4188,7 @@ class SCMTreeDataSource implements IAsyncDataSource(), + historyItems2: new Map(), historyItemChanges: new Map() }; } @@ -3991,3 +4278,28 @@ export class SCMActionButton implements IDisposable { } } } + +// Override styles in selections.ts +registerThemingParticipant((theme, collector) => { + const selectionBackgroundColor = theme.getColor(selectionBackground); + + if (selectionBackgroundColor) { + // Override inactive selection bg + const inputBackgroundColor = theme.getColor(inputBackground); + if (inputBackgroundColor) { + collector.addRule(`.scm-view .scm-editor-container .monaco-editor-background { background-color: ${inputBackgroundColor}; } `); + collector.addRule(`.scm-view .scm-editor-container .monaco-editor .selected-text { background-color: ${inputBackgroundColor.transparent(0.4)}; }`); + } + + // Override selected fg + const inputForegroundColor = theme.getColor(inputForeground); + if (inputForegroundColor) { + collector.addRule(`.scm-view .scm-editor-container .monaco-editor .view-line span.inline-selected-text { color: ${inputForegroundColor}; }`); + } + + collector.addRule(`.scm-view .scm-editor-container .monaco-editor .focused .selected-text { background-color: ${selectionBackgroundColor}; }`); + } else { + // Use editor selection color if theme has not set a selection background color + collector.addRule(`.scm-view .scm-editor-container .monaco-editor .focused .selected-text { background-color: ${theme.getColor(editorSelectionBackground)}; }`); + } +}); diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 00c333886a6..ed24dc67bf0 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'vs/base/common/path'; -import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; +import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMHistoryItemViewModelTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { IMenu, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -62,6 +62,10 @@ export function isSCMHistoryItemTreeElement(element: any): element is SCMHistory (element as SCMHistoryItemTreeElement).type === 'historyItem'; } +export function isSCMHistoryItemViewModelTreeElement(element: any): element is SCMHistoryItemViewModelTreeElement { + return (element as SCMHistoryItemViewModelTreeElement).type === 'historyItem2'; +} + export function isSCMHistoryItemChangeTreeElement(element: any): element is SCMHistoryItemChangeTreeElement { return (element as SCMHistoryItemChangeTreeElement).type === 'historyItemChange'; } @@ -173,3 +177,7 @@ export function getActionViewItemProvider(instaService: IInstantiationService): export function getProviderKey(provider: ISCMProvider): string { return `${provider.contextValue}:${provider.label}${provider.rootUri ? `:${provider.rootUri.toString()}` : ''}`; } + +export function getRepositoryResourceCount(provider: ISCMProvider): number { + return provider.groups.reduce((r, g) => r + g.resources.length, 0); +} diff --git a/src/vs/workbench/contrib/scm/browser/workingSet.ts b/src/vs/workbench/contrib/scm/browser/workingSet.ts index 498b667390e..274bd713910 100644 --- a/src/vs/workbench/contrib/scm/browser/workingSet.ts +++ b/src/vs/workbench/contrib/scm/browser/workingSet.ts @@ -3,14 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; -import { DisposableMap, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore } from 'vs/base/common/lifecycle'; +import { autorun, autorunWithStore } from 'vs/base/common/observable'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { getProviderKey } from 'vs/workbench/contrib/scm/browser/util'; import { ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; import { IEditorGroupsService, IEditorWorkingSet } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; type ISCMSerializedWorkingSet = { readonly providerKey: string; @@ -23,83 +25,76 @@ interface ISCMRepositoryWorkingSet { readonly editorWorkingSets: Map; } -export class SCMWorkingSetController implements IWorkbenchContribution { +export class SCMWorkingSetController extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.scmWorkingSets'; private _workingSets!: Map; + private _enabledConfig = observableConfigValue('scm.workingSets.enabled', false, this.configurationService); + private readonly _repositoryDisposables = new DisposableMap(); - private readonly _scmServiceDisposables = new DisposableStore(); - private readonly _disposables = new DisposableStore(); constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @ISCMService private readonly scmService: ISCMService, - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService ) { - const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.workingSets.enabled'), this._disposables); - this._disposables.add(Event.runAndSubscribe(onDidChangeConfiguration, () => this._onDidChangeConfiguration())); - } + super(); - private _onDidChangeConfiguration(): void { - if (!this.configurationService.getValue('scm.workingSets.enabled')) { - this.storageService.remove('scm.workingSets', StorageScope.WORKSPACE); + this._store.add(autorunWithStore((reader, store) => { + if (!this._enabledConfig.read(reader)) { + this.storageService.remove('scm.workingSets', StorageScope.WORKSPACE); + this._repositoryDisposables.clearAndDisposeAll(); + return; + } - this._scmServiceDisposables.clear(); - this._repositoryDisposables.clearAndDisposeAll(); + this._workingSets = this._loadWorkingSets(); - return; - } + this.scmService.onDidAddRepository(this._onDidAddRepository, this, store); + this.scmService.onDidRemoveRepository(this._onDidRemoveRepository, this, store); - this._workingSets = this._loadWorkingSets(); - - this.scmService.onDidAddRepository(this._onDidAddRepository, this, this._scmServiceDisposables); - this.scmService.onDidRemoveRepository(this._onDidRemoveRepository, this, this._scmServiceDisposables); - - for (const repository of this.scmService.repositories) { - this._onDidAddRepository(repository); - } + for (const repository of this.scmService.repositories) { + this._onDidAddRepository(repository); + } + })); } private _onDidAddRepository(repository: ISCMRepository): void { const disposables = new DisposableStore(); - disposables.add(Event.runAndSubscribe(repository.provider.onDidChangeHistoryProvider, () => { - if (!repository.provider.historyProvider) { + disposables.add(autorun(async reader => { + const historyProvider = repository.provider.historyProviderObs.read(reader); + const currentHistoryItemGroupId = historyProvider?.currentHistoryItemGroupObs.read(reader)?.id; + + if (!currentHistoryItemGroupId) { return; } - disposables.add(Event.runAndSubscribe(repository.provider.historyProvider.onDidChangeCurrentHistoryItemGroup, async () => { - if (!repository.provider.historyProvider?.currentHistoryItemGroup?.id) { - return; - } + const providerKey = getProviderKey(repository.provider); + const repositoryWorkingSets = this._workingSets.get(providerKey); - const providerKey = getProviderKey(repository.provider); - const currentHistoryItemGroupId = repository.provider.historyProvider.currentHistoryItemGroup.id; - const repositoryWorkingSets = this._workingSets.get(providerKey); + if (!repositoryWorkingSets) { + this._workingSets.set(providerKey, { currentHistoryItemGroupId, editorWorkingSets: new Map() }); + return; + } - if (!repositoryWorkingSets) { - this._workingSets.set(providerKey, { currentHistoryItemGroupId, editorWorkingSets: new Map() }); - return; - } + // Editors for the current working set are automatically restored + if (repositoryWorkingSets.currentHistoryItemGroupId === currentHistoryItemGroupId) { + return; + } - if (repositoryWorkingSets.currentHistoryItemGroupId === currentHistoryItemGroupId) { - return; - } + // Save the working set + this._saveWorkingSet(providerKey, currentHistoryItemGroupId, repositoryWorkingSets); - // Save the working set - this._saveWorkingSet(providerKey, currentHistoryItemGroupId, repositoryWorkingSets); - - // Restore the working set - await this._restoreWorkingSet(providerKey, currentHistoryItemGroupId); - })); + // Restore the working set + await this._restoreWorkingSet(providerKey, currentHistoryItemGroupId); })); this._repositoryDisposables.set(repository, disposables); } private _onDidRemoveRepository(repository: ISCMRepository): void { - this._workingSets.delete(getProviderKey(repository.provider)); this._repositoryDisposables.deleteAndDispose(repository); } @@ -147,13 +142,18 @@ export class SCMWorkingSetController implements IWorkbenchContribution { } if (editorWorkingSetId) { - await this.editorGroupsService.applyWorkingSet(editorWorkingSetId); + // Applying a working set can be the result of a user action that has been + // initiated from the terminal (ex: switching branches). As such, we want + // to preserve the focus in the terminal. This does not cover the scenario + // in which the terminal is in the editor part. + const preserveFocus = this.layoutService.hasFocus(Parts.PANEL_PART); + + await this.editorGroupsService.applyWorkingSet(editorWorkingSetId, { preserveFocus }); } } - dispose(): void { + override dispose(): void { this._repositoryDisposables.dispose(); - this._scmServiceDisposables.dispose(); - this._disposables.dispose(); + super.dispose(); } } diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 2cb81effd91..83d983824f3 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; +import { IObservable } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IMenu } from 'vs/platform/actions/common/actions'; @@ -22,8 +23,10 @@ export interface ISCMHistoryProvider { get currentHistoryItemGroup(): ISCMHistoryItemGroup | undefined; set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined); + readonly currentHistoryItemGroupObs: IObservable; provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise; + provideHistoryItems2(options: ISCMHistoryOptions): Promise; provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined>; @@ -33,18 +36,21 @@ export interface ISCMHistoryProviderCacheEntry { readonly incomingHistoryItemGroup: SCMHistoryItemGroupTreeElement | undefined; readonly outgoingHistoryItemGroup: SCMHistoryItemGroupTreeElement | undefined; readonly historyItems: Map; + readonly historyItems2: Map; readonly historyItemChanges: Map; } export interface ISCMHistoryOptions { readonly cursor?: string; readonly limit?: number | { id?: string }; + readonly historyItemGroupIds?: readonly string[]; } export interface ISCMHistoryItemGroup { readonly id: string; readonly name: string; - readonly base?: Omit; + readonly base?: Omit, 'remote'>; + readonly remote?: Omit, 'remote'>; } export interface SCMHistoryItemGroupTreeElement { @@ -66,6 +72,11 @@ export interface ISCMHistoryItemStatistics { readonly deletions: number; } +export interface ISCMHistoryItemLabel { + readonly title: string; + readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; +} + export interface ISCMHistoryItem { readonly id: string; readonly parentIds: string[]; @@ -74,6 +85,24 @@ export interface ISCMHistoryItem { readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; readonly timestamp?: number; readonly statistics?: ISCMHistoryItemStatistics; + readonly labels?: ISCMHistoryItemLabel[]; +} + +export interface ISCMHistoryItemGraphNode { + readonly id: string; + readonly color: number; +} + +export interface ISCMHistoryItemViewModel { + readonly historyItem: ISCMHistoryItem; + readonly inputSwimlanes: ISCMHistoryItemGraphNode[]; + readonly outputSwimlanes: ISCMHistoryItemGraphNode[]; +} + +export interface SCMHistoryItemViewModelTreeElement { + readonly repository: ISCMRepository; + readonly historyItemViewModel: ISCMHistoryItemViewModel; + readonly type: 'historyItem2'; } export interface SCMHistoryItemTreeElement extends ISCMHistoryItem { diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 3fe568b77d5..5bcd1c6fbe7 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -15,6 +15,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { ResourceTree } from 'vs/base/common/resourceTree'; import { ISCMHistoryProvider, ISCMHistoryProviderMenus } from 'vs/workbench/contrib/scm/common/history'; import { ITextModel } from 'vs/editor/common/model'; +import { IObservable } from 'vs/base/common/observable'; export const VIEWLET_ID = 'workbench.view.scm'; export const VIEW_PANE_ID = 'workbench.scm'; @@ -72,15 +73,14 @@ export interface ISCMProvider extends IDisposable { readonly rootUri?: URI; readonly inputBoxTextModel: ITextModel; - readonly count?: number; - readonly commitTemplate: string; + readonly count: IObservable; + readonly commitTemplate: IObservable; readonly historyProvider?: ISCMHistoryProvider; - readonly onDidChangeCommitTemplate: Event; + readonly historyProviderObs: IObservable; readonly onDidChangeHistoryProvider: Event; - readonly onDidChangeStatusBarCommands?: Event; readonly acceptInputCommand?: Command; readonly actionButton?: ISCMActionButtonDescriptor; - readonly statusBarCommands?: readonly Command[]; + readonly statusBarCommands: IObservable; readonly onDidChange: Event; getOriginalResource(uri: URI): Promise; @@ -173,7 +173,9 @@ export interface ISCMService { readonly repositoryCount: number; registerSCMProvider(provider: ISCMProvider): ISCMRepository; + getRepository(id: string): ISCMRepository | undefined; + getRepository(resource: URI): ISCMRepository | undefined; } export interface ISCMTitleMenu { diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 762dc9ed1b6..b341a2029ae 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator, ISCMInputChangeEvent, SCMInputChangeReason, InputValidationType, IInputValidation } from './scm'; import { ILogService } from 'vs/platform/log/common/log'; @@ -15,8 +15,10 @@ import { ResourceMap } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { Iterable } from 'vs/base/common/iterator'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { Schemas } from 'vs/base/common/network'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -class SCMInput implements ISCMInput { +class SCMInput extends Disposable implements ISCMInput { private _value = ''; @@ -104,9 +106,11 @@ class SCMInput implements ISCMInput { readonly repository: ISCMRepository, private readonly history: SCMInputHistory ) { + super(); + if (this.repository.provider.rootUri) { this.historyNavigator = history.getHistory(this.repository.provider.label, this.repository.provider.rootUri); - this.history.onWillSaveHistory(event => { + this._register(this.history.onWillSaveHistory(event => { if (this.historyNavigator.isAtEnd()) { this.saveValue(); } @@ -116,7 +120,7 @@ class SCMInput implements ISCMInput { } this.didChangeHistory = false; - }); + })); } else { // in memory only this.historyNavigator = new HistoryNavigator2([''], 100); } @@ -130,7 +134,7 @@ class SCMInput implements ISCMInput { } if (!transient) { - this.historyNavigator.add(this._value); + this.historyNavigator.replaceLast(this._value); this.historyNavigator.add(value); this.didChangeHistory = true; } @@ -362,7 +366,8 @@ export class SCMService implements ISCMService { @ILogService private readonly logService: ILogService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IContextKeyService contextKeyService: IContextKeyService, - @IStorageService storageService: IStorageService + @IStorageService storageService: IStorageService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { this.inputHistory = new SCMInputHistory(storageService, workspaceContextService); this.providerCount = contextKeyService.createKey('scm.providerCount', 0); @@ -389,8 +394,36 @@ export class SCMService implements ISCMService { return repository; } - getRepository(id: string): ISCMRepository | undefined { - return this._repositories.get(id); - } + getRepository(id: string): ISCMRepository | undefined; + getRepository(resource: URI): ISCMRepository | undefined; + getRepository(idOrResource: string | URI): ISCMRepository | undefined { + if (typeof idOrResource === 'string') { + return this._repositories.get(idOrResource); + } + if (idOrResource.scheme !== Schemas.file && + idOrResource.scheme !== Schemas.vscodeRemote) { + return undefined; + } + + let bestRepository: ISCMRepository | undefined = undefined; + let bestMatchLength = Number.POSITIVE_INFINITY; + + for (const repository of this.repositories) { + const root = repository.provider.rootUri; + + if (!root) { + continue; + } + + const path = this.uriIdentityService.extUri.relativePath(root, idOrResource); + + if (path && !/^\.\./.test(path) && path.length < bestMatchLength) { + bestRepository = repository; + bestMatchLength = path.length; + } + } + + return bestRepository; + } } diff --git a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts new file mode 100644 index 00000000000..5c64ccda402 --- /dev/null +++ b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts @@ -0,0 +1,503 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory'; +import { ISCMHistoryItem } from 'vs/workbench/contrib/scm/common/history'; + +suite('toISCMHistoryItemViewModelArray', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('empty graph', () => { + const viewModels = toISCMHistoryItemViewModelArray([]); + + assert.strictEqual(viewModels.length, 0); + }); + + + /** + * * a + */ + + test('single commit', () => { + const models = [ + { id: 'a', parentIds: [], message: '' }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 1); + + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + assert.strictEqual(viewModels[0].outputSwimlanes.length, 0); + }); + + /** + * * a(b) + * * b(c) + * * c(d) + * * d(e) + * * e + */ + test('linear graph', () => { + const models = [ + { id: 'a', parentIds: ['b'] }, + { id: 'b', parentIds: ['c'] }, + { id: 'c', parentIds: ['d'] }, + { id: 'd', parentIds: ['e'] }, + { id: 'e', parentIds: [] }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 5); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + + // node b + assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + + // node c + assert.strictEqual(viewModels[2].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + + // node d + assert.strictEqual(viewModels[3].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + + // node e + assert.strictEqual(viewModels[4].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 0); + }); + + /** + * * a(b) + * * b(c,d) + * |\ + * | * d(c) + * |/ + * * c(e) + * * e(f) + */ + test('merge commit (single commit in topic branch)', () => { + const models = [ + { id: 'a', parentIds: ['b'] }, + { id: 'b', parentIds: ['c', 'd'] }, + { id: 'd', parentIds: ['c'] }, + { id: 'c', parentIds: ['e'] }, + { id: 'e', parentIds: ['f'] }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 5); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + + // node b + assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node d + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + + // node e + assert.strictEqual(viewModels[4].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + }); + + /** + * * a(b,c) + * |\ + * | * c(d) + * * | b(e) + * * | e(f) + * * | f(d) + * |/ + * * d(g) + */ + test('merge commit (multiple commits in topic branch)', () => { + const models = [ + { id: 'a', parentIds: ['b', 'c'] }, + { id: 'c', parentIds: ['d'] }, + { id: 'b', parentIds: ['e'] }, + { id: 'e', parentIds: ['f'] }, + { id: 'f', parentIds: ['d'] }, + { id: 'd', parentIds: ['g'] }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 6); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node b + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + + // node e + assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 1); + + // node f + assert.strictEqual(viewModels[4].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 1); + + // node d + assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + }); + + /** + * * a(b,c) + * |\ + * | * c(b) + * |/ + * * b(d,e) + * |\ + * | * e(f) + * | * f(g) + * * | d(h) + */ + test('create brach from merge commit', () => { + const models = [ + { id: 'a', parentIds: ['b', 'c'] }, + { id: 'c', parentIds: ['b'] }, + { id: 'b', parentIds: ['d', 'e'] }, + { id: 'e', parentIds: ['f'] }, + { id: 'f', parentIds: ['g'] }, + { id: 'd', parentIds: ['h'] }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 6); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node b + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 2); + + // node e + assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 2); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'f'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 2); + + // node f + assert.strictEqual(viewModels[4].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'f'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 2); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 2); + + // node d + assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 2); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'h'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, 2); + }); + + + /** + * * a(b,c) + * |\ + * | * c(d) + * * | b(e,f) + * |\| + * | |\ + * | | * f(g) + * * | | e(g) + * | * | d(g) + * |/ / + * | / + * |/ + * * g(h) + */ + test('create multiple branches from a commit', () => { + const models = [ + { id: 'a', parentIds: ['b', 'c'] }, + { id: 'c', parentIds: ['d'] }, + { id: 'b', parentIds: ['e', 'f'] }, + { id: 'f', parentIds: ['g'] }, + { id: 'e', parentIds: ['g'] }, + { id: 'd', parentIds: ['g'] }, + { id: 'g', parentIds: ['h'] }, + ] as ISCMHistoryItem[]; + + const viewModels = toISCMHistoryItemViewModelArray(models); + + assert.strictEqual(viewModels.length, 7); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node b + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[2].outputSwimlanes[2].id, 'f'); + assert.strictEqual(viewModels[2].outputSwimlanes[2].color, 2); + + // node f + assert.strictEqual(viewModels[3].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[3].inputSwimlanes[2].id, 'f'); + assert.strictEqual(viewModels[3].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[3].outputSwimlanes[2].color, 2); + + // node e + assert.strictEqual(viewModels[4].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[4].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[4].outputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[4].outputSwimlanes[2].color, 2); + + // node d + assert.strictEqual(viewModels[5].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[5].inputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[5].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[5].outputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[2].color, 2); + + // node g + assert.strictEqual(viewModels[6].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[6].inputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[6].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[6].inputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[6].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[6].inputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[6].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[6].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[6].outputSwimlanes[0].id, 'h'); + assert.strictEqual(viewModels[6].outputSwimlanes[0].color, 0); + }); +}); diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index c9a116bdbfe..74f304ba7a3 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -319,13 +319,12 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider | Promise> | FastAndSlowPicks { - const configuration = { ...this.configuration, includeSymbols: options.includeSymbols ?? this.configuration.includeSymbols }; const query = prepareQuery(filter); // Return early if we have editor symbol picks. We support this by: // - having a previously active global pick (e.g. a file) // - the user typing `@` to start the local symbol query - if (options.enableEditorSymbolSearch && options.includeSymbols) { + if (options.enableEditorSymbolSearch) { const editorSymbolPicks = this.getEditorSymbolPicks(query, disposables, token); if (editorSymbolPicks) { return editorSymbolPicks; @@ -397,7 +396,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider options.filter?.(p)); } @@ -406,7 +405,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider 0 ? [ - { type: 'separator', label: configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") }, + { type: 'separator', label: this.configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") }, ...additionalPicks ] : []; })(), diff --git a/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts b/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts index b39946294c8..4ef1cdae76e 100644 --- a/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts +++ b/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts @@ -13,7 +13,7 @@ import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/note import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; import { INotebookCellMatchWithModel, INotebookFileMatchWithModel, contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers'; -import { ITextQuery, QueryType, ISearchProgressItem, ISearchComplete, ISearchConfigurationProperties, pathIncludedInQuery, ISearchService, IFolderQuery } from 'vs/workbench/services/search/common/search'; +import { ITextQuery, QueryType, ISearchProgressItem, ISearchComplete, ISearchConfigurationProperties, pathIncludedInQuery, ISearchService, IFolderQuery, DEFAULT_MAX_SEARCH_RESULTS } from 'vs/workbench/services/search/common/search'; import * as arrays from 'vs/base/common/arrays'; import { isNumber } from 'vs/base/common/types'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; @@ -232,7 +232,7 @@ export class NotebookSearchService implements INotebookSearchService { if (!widget.hasModel()) { continue; } - const askMax = isNumber(query.maxResults) ? query.maxResults + 1 : Number.MAX_SAFE_INTEGER; + const askMax = (isNumber(query.maxResults) ? query.maxResults : DEFAULT_MAX_SEARCH_RESULTS) + 1; const uri = widget.viewModel!.uri; if (!pathIncludedInQuery(query, uri.fsPath)) { diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 26e2d6cc45f..1da6d744da9 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -26,7 +26,7 @@ import { registerContributions as searchWidgetContributions } from 'vs/workbench import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; import { ISearchHistoryService, SearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; import { ISearchViewModelWorkbenchService, SearchViewModelWorkbenchService } from 'vs/workbench/contrib/search/browser/searchModel'; -import { SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID } from 'vs/workbench/services/search/common/search'; +import { SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID, DEFAULT_MAX_SEARCH_RESULTS } from 'vs/workbench/services/search/common/search'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { assertType } from 'vs/base/common/types'; import { getWorkspaceSymbols, IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search'; @@ -183,13 +183,13 @@ configurationRegistry.registerConfiguration({ }, 'search.useGlobalIgnoreFiles': { type: 'boolean', - markdownDescription: nls.localize('useGlobalIgnoreFiles', "Controls whether to use your global gitignore file (for example, from `$HOME/.config/git/ignore`) when searching for files. Requires `#search.useIgnoreFiles#` to be enabled."), + markdownDescription: nls.localize('useGlobalIgnoreFiles', "Controls whether to use your global gitignore file (for example, from `$HOME/.config/git/ignore`) when searching for files. Requires {0} to be enabled.", '`#search.useIgnoreFiles#`'), default: false, scope: ConfigurationScope.RESOURCE }, 'search.useParentIgnoreFiles': { type: 'boolean', - markdownDescription: nls.localize('useParentIgnoreFiles', "Controls whether to use `.gitignore` and `.ignore` files in parent directories when searching for files. Requires `#search.useIgnoreFiles#` to be enabled."), + markdownDescription: nls.localize('useParentIgnoreFiles', "Controls whether to use `.gitignore` and `.ignore` files in parent directories when searching for files. Requires {0} to be enabled.", '`#search.useIgnoreFiles#`'), default: false, scope: ConfigurationScope.RESOURCE }, @@ -198,6 +198,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('search.quickOpen.includeSymbols', "Whether to include results from a global symbol search in the file results for Quick Open."), default: false }, + 'search.ripgrep.maxThreads': { + type: 'number', + description: nls.localize('search.ripgrep.maxThreads', "Number of threads to use for searching. When set to 0, the engine automatically determines this value."), + default: 0 + }, 'search.quickOpen.includeHistory': { type: 'boolean', description: nls.localize('search.quickOpen.includeHistory', "Whether to include results from recently opened files in the file results for Quick Open."), @@ -238,7 +243,7 @@ configurationRegistry.registerConfiguration({ }, 'search.maxResults': { type: ['number', 'null'], - default: 20000, + default: DEFAULT_MAX_SEARCH_RESULTS, markdownDescription: nls.localize('search.maxResults', "Controls the maximum number of search results, this can be set to `null` (empty) to return unlimited results.") }, 'search.collapseResults': { diff --git a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts index e848ced9bb3..a16f5a19f44 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts @@ -49,6 +49,7 @@ export interface IFindInFilesArgs { matchWholeWord?: boolean; useExcludeSettingsAndIgnoreFiles?: boolean; onlyOpenEditors?: boolean; + showIncludesExcludes?: boolean; } //#endregion @@ -208,6 +209,7 @@ registerAction2(class FindInFilesAction extends Action2 { matchWholeWord: { 'type': 'boolean' }, useExcludeSettingsAndIgnoreFiles: { 'type': 'boolean' }, onlyOpenEditors: { 'type': 'boolean' }, + showIncludesExcludes: { 'type': 'boolean' } } } }, @@ -407,6 +409,9 @@ export async function findInFilesCommand(accessor: ServicesAccessor, _args: IFin updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: typeof args.replace !== 'string' }); } openedView.setSearchParameters(args); + if (typeof args.showIncludesExcludes === 'boolean') { + openedView.toggleQueryDetails(false, args.showIncludesExcludes); + } openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); } diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 82c166d796d..764f8966664 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -41,7 +41,7 @@ import { contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches, I import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; import { rawCellPrefix, INotebookCellMatchNoModel, isINotebookFileMatchNoModel } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; -import { IAITextQuery, IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, QueryType, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { DEFAULT_MAX_SEARCH_RESULTS, IAITextQuery, IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, QueryType, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { getTextSearchMatchWithModelContext, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; import { CellFindMatchModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; @@ -529,7 +529,7 @@ export class FileMatch extends Disposable implements IFileMatch { const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null; const matches = this._model - .findMatches(this._query.pattern, this._model.getFullModelRange(), !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); + .findMatches(this._query.pattern, this._model.getFullModelRange(), !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? DEFAULT_MAX_SEARCH_RESULTS); this.updateMatches(matches, true, this._model, false); } @@ -550,7 +550,7 @@ export class FileMatch extends Disposable implements IFileMatch { oldMatches.forEach(match => this._textMatches.delete(match.id())); const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null; - const matches = this._model.findMatches(this._query.pattern, range, !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); + const matches = this._model.findMatches(this._query.pattern, range, !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? DEFAULT_MAX_SEARCH_RESULTS); this.updateMatches(matches, modelChange, this._model, false); // await this.updateMatchesForEditorWidget(); diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index d7c088c442e..121512e94be 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -136,7 +136,7 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree SearchContext.FileFocusKey.bindTo(contextKeyServiceMain).set(false); SearchContext.FolderFocusKey.bindTo(contextKeyServiceMain).set(true); - const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain])); + const instantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain]))); const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { menuOptions: { shouldForwardArgs: true @@ -365,7 +365,7 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender templateData.after.textContent = preview.after; const title = (preview.fullBefore + (replace ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999); - templateData.disposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), templateData.parent, title)); + templateData.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), templateData.parent, title)); SearchContext.IsEditableItemKey.bindTo(templateData.contextKeyService).set(!(match instanceof MatchInNotebook && match.isReadonly())); @@ -377,7 +377,7 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender templateData.lineNumber.classList.toggle('show', (numLines > 0) || showLineNumbers); templateData.lineNumber.textContent = lineNumberStr + extraLinesStr; - templateData.disposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), templateData.lineNumber, this.getMatchTitle(match, showLineNumbers))); + templateData.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), templateData.lineNumber, this.getMatchTitle(match, showLineNumbers))); templateData.actions.context = { viewer: this.searchView.getControl(), element: match } satisfies ISearchActionContext; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 69e8a910540..662d7fb9019 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -166,6 +166,9 @@ export class SearchView extends ViewPane { private _onSearchResultChangedDisposable: IDisposable | undefined; + private _stashedQueryDetailsVisibility: boolean | undefined = undefined; + private _stashedReplaceVisibility: boolean | undefined = undefined; + constructor( options: IViewPaneOptions, @IFileService private readonly fileService: IFileService, @@ -237,8 +240,8 @@ export class SearchView extends ViewPane { this.inputPatternExclusionsFocused = Constants.SearchContext.PatternExcludesFocusedKey.bindTo(this.contextKeyService); this.isEditableItem = Constants.SearchContext.IsEditableItemKey.bindTo(this.contextKeyService); - this.instantiationService = this.instantiationService.createChild( - new ServiceCollection([IContextKeyService, this.contextKeyService])); + this.instantiationService = this._register(this.instantiationService.createChild( + new ServiceCollection([IContextKeyService, this.contextKeyService]))); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('search.sortOrder')) { @@ -327,7 +330,32 @@ export class SearchView extends ViewPane { if (visible === this.aiResultsVisible) { return; } + + if (visible) { + this._stashedQueryDetailsVisibility = this._queryDetailsHidden(); + this._stashedReplaceVisibility = this.searchWidget.isReplaceShown(); + + this.searchWidget.toggleReplace(false); + this.toggleQueryDetailsButton.style.display = 'none'; + + this.searchWidget.replaceButtonVisibility = false; + this.toggleQueryDetails(undefined, false); + } else { + this.toggleQueryDetailsButton.style.display = ''; + this.searchWidget.replaceButtonVisibility = true; + + if (this._stashedReplaceVisibility) { + this.searchWidget.toggleReplace(this._stashedReplaceVisibility); + } + + if (this._stashedQueryDetailsVisibility) { + this.toggleQueryDetails(undefined, this._stashedQueryDetailsVisibility); + } + } + this.aiResultsVisible = visible; + + if (this.viewModel.searchResult.isEmpty()) { return; } @@ -336,9 +364,8 @@ export class SearchView extends ViewPane { this.model.cancelAISearch(); if (visible) { await this.model.addAIResults(); - } else { - this.searchWidget.toggleReplace(false); } + this.onSearchResultsChanged(); this.onSearchComplete(() => { }, undefined, undefined, this.viewModel.searchResult.getCachedSearchComplete(visible)); } @@ -455,7 +482,7 @@ export class SearchView extends ViewPane { // Toggle query details button this.toggleQueryDetailsButton = dom.append(this.queryDetails, $('.more' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button' })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, nls.localize('moreSearch', "Toggle Search Details"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, nls.localize('moreSearch', "Toggle Search Details"))); this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.CLICK, e => { dom.EventHelper.stop(e); @@ -1482,6 +1509,10 @@ export class SearchView extends ViewPane { } } + private _queryDetailsHidden() { + return this.queryDetails.classList.contains('more'); + } + searchInFolders(folderPaths: string[] = []): void { this._searchWithIncludeOrExclude(true, folderPaths); } @@ -2222,7 +2253,7 @@ class SearchLinkButton extends Disposable { constructor(label: string, handler: (e: dom.EventLike) => unknown, hoverService: IHoverService, tooltip?: string) { super(); this.element = $('a.pointer', { tabindex: 0 }, label); - this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.element, tooltip)); + this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, tooltip)); this.addEventHandlers(handler); } diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 766990702f8..f8beab04b54 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -16,7 +17,6 @@ import { Delayer } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { CONTEXT_FIND_WIDGET_NOT_VISIBLE } from 'vs/editor/contrib/find/browser/findModel'; -import * as nls from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -44,6 +44,7 @@ import { GroupModelChangeKind } from 'vs/workbench/common/editor'; import { SearchFindInput } from 'vs/workbench/contrib/search/browser/searchFindInput'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { NotebookFindScopeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; /** Specified in searchview.css */ const SingleLineInputHeight = 26; @@ -204,7 +205,8 @@ export class SearchWidget extends Widget { notebookOptions.isInNotebookMarkdownInput, notebookOptions.isInNotebookMarkdownPreview, notebookOptions.isInNotebookCellInput, - notebookOptions.isInNotebookCellOutput + notebookOptions.isInNotebookCellOutput, + { findScopeType: NotebookFindScopeType.None } )); this._register( @@ -226,13 +228,13 @@ export class SearchWidget extends Widget { this.render(container, options); - this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.accessibilitySupport')) { this.updateAccessibilitySupport(); } - }); + })); - this.accessibilityService.onDidChangeScreenReaderOptimized(() => this.updateAccessibilitySupport()); + this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this.updateAccessibilitySupport())); this.updateAccessibilitySupport(); } @@ -350,6 +352,12 @@ export class SearchWidget extends Widget { this.searchInput?.focusOnRegex(); } + set replaceButtonVisibility(val: boolean) { + if (this.toggleReplaceButton) { + this.toggleReplaceButton.element.style.display = val ? '' : 'none'; + } + } + private render(container: HTMLElement, options: ISearchWidgetOptions): void { this.domNode = dom.append(container, dom.$('.search-widget')); this.domNode.style.position = 'relative'; @@ -419,7 +427,7 @@ export class SearchWidget extends Widget { ) ); - this.searchInput.onKeyDown((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyDown(keyboardEvent)); + this._register(this.searchInput.onKeyDown((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyDown(keyboardEvent))); this.searchInput.setValue(options.value || ''); this.searchInput.setRegex(!!options.isRegex); this.searchInput.setCaseSensitive(!!options.isCaseSensitive); @@ -521,7 +529,7 @@ export class SearchWidget extends Widget { } })); - this.replaceInput.onKeyDown((keyboardEvent) => this.onReplaceInputKeyDown(keyboardEvent)); + this._register(this.replaceInput.onKeyDown((keyboardEvent) => this.onReplaceInputKeyDown(keyboardEvent))); this.replaceInput.setValue(options.replaceValue || ''); this._register(this.replaceInput.inputBox.onDidChange(() => this._onReplaceValueChanged.fire())); this._register(this.replaceInput.inputBox.onDidHeightChange(() => this._onDidHeightChange.fire())); @@ -663,6 +671,25 @@ export class SearchWidget extends Widget { else if (keyboardEvent.equals(KeyCode.DownArrow)) { stopPropagationForMultiLineDownwards(keyboardEvent, this.searchInput?.getValue() ?? '', this.searchInput?.domNode.querySelector('textarea') ?? null); } + + else if (keyboardEvent.equals(KeyCode.PageUp)) { + const inputElement = this.searchInput?.inputBox.inputElement; + if (inputElement) { + inputElement.setSelectionRange(0, 0); + inputElement.focus(); + keyboardEvent.preventDefault(); + } + } + + else if (keyboardEvent.equals(KeyCode.PageDown)) { + const inputElement = this.searchInput?.inputBox.inputElement; + if (inputElement) { + const endOfText = inputElement.value.length; + inputElement.setSelectionRange(endOfText, endOfText); + inputElement.focus(); + keyboardEvent.preventDefault(); + } + } } private onCaseSensitiveKeyDown(keyboardEvent: IKeyboardEvent) { diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts index 175175d8784..3ec70181227 100644 --- a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -25,7 +25,7 @@ import { IMatch } from 'vs/base/common/filters'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; -interface ISymbolQuickPickItem extends IPickerQuickAccessItem, IQuickPickItemWithResource { +export interface ISymbolQuickPickItem extends IPickerQuickAccessItem, IQuickPickItemWithResource { score?: number; symbol?: IWorkspaceSymbol; } diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index 9b3b9e4f4dd..2e98bb36cb6 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Keybinding } from 'vs/base/common/keybindings'; import { OS } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts b/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts index 17f9e88b338..5c79a6d8046 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import * as arrays from 'vs/base/common/arrays'; import { DeferredPromise, timeout } from 'vs/base/common/async'; @@ -644,4 +644,3 @@ suite('SearchModel', () => { return notebookEditorWidgetService; } }); - diff --git a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts index a90875c1384..f30dcaf0de3 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { FindMatch, IReadonlyTextBuffer } from 'vs/editor/common/model'; import { IFileMatch, ISearchRange, ITextSearchMatch, QueryType } from 'vs/workbench/services/search/common/search'; diff --git a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts index e71fab4b131..600cc67f97a 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Match, FileMatch, SearchResult, SearchModel, FolderMatch, CellMatch } from 'vs/workbench/contrib/search/browser/searchModel'; diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 4feb3d343ed..c35d9b87da6 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IModelService } from 'vs/editor/common/services/model'; diff --git a/src/vs/workbench/contrib/search/test/common/cacheState.test.ts b/src/vs/workbench/contrib/search/test/common/cacheState.test.ts index a986de9b0bf..ea1241bed90 100644 --- a/src/vs/workbench/contrib/search/test/common/cacheState.test.ts +++ b/src/vs/workbench/contrib/search/test/common/cacheState.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as errors from 'vs/base/common/errors'; import { QueryType, IFileQuery } from 'vs/workbench/services/search/common/search'; import { FileQueryCacheState } from 'vs/workbench/contrib/search/common/cacheState'; diff --git a/src/vs/workbench/contrib/search/test/common/extractRange.test.ts b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts index 4651bbc6403..0758b486cf6 100644 --- a/src/vs/workbench/contrib/search/test/common/extractRange.test.ts +++ b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { extractRangeFromFilter } from 'vs/workbench/contrib/search/common/search'; @@ -98,4 +98,3 @@ suite('extractRangeFromFilter', () => { } }); }); - diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index c0f0f27dbfa..c3577b4e873 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -141,7 +141,7 @@ export class SearchEditor extends AbstractTextCodeEditor this.createQueryEditor( this.queryEditorContainer, - this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])), + this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService]))), SearchContext.InputBoxFocusedKey.bindTo(scopedContextKeyService) ); } @@ -166,7 +166,7 @@ export class SearchEditor extends AbstractTextCodeEditor // Toggle query details button this.toggleQueryDetailsButton = DOM.append(this.includesExcludesContainer, DOM.$('.expand' + ThemeIcon.asCSSSelector(searchDetailsIcon), { tabindex: 0, role: 'button' })); - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, localize('moreSearch', "Toggle Search Details"))); + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this.toggleQueryDetailsButton, localize('moreSearch', "Toggle Search Details"))); this._register(DOM.addDisposableListener(this.toggleQueryDetailsButton, DOM.EventType.CLICK, e => { DOM.EventHelper.stop(e); this.toggleIncludesExcludes(); @@ -776,7 +776,7 @@ export class SearchEditor extends AbstractTextCodeEditor } } -const searchEditorTextInputBorder = registerColor('searchEditor.textInputBorder', { dark: inputBorder, light: inputBorder, hcDark: inputBorder, hcLight: inputBorder }, localize('textInputBoxBorder', "Search editor text input box border.")); +const searchEditorTextInputBorder = registerColor('searchEditor.textInputBorder', inputBorder, localize('textInputBoxBorder', "Search editor text input box border.")); function findNextRange(matchRanges: Range[], currentPosition: Position) { for (const matchRange of matchRanges) { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index 2085c52e041..68b40814a20 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -128,7 +128,7 @@ export class SearchEditorInput extends EditorInput { } this.memento = new Memento(SearchEditorInput.ID, storageService); - storageService.onWillSaveState(() => this.memento.saveMemento()); + this._register(storageService.onWillSaveState(() => this.memento.saveMemento())); const input = this; const workingCopyAdapter = new class implements IWorkingCopy { diff --git a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts index 77dad6efa4a..198ec6d581b 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts @@ -8,6 +8,7 @@ import { extname } from 'vs/base/common/path'; import { basename, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { getIconClassesForLanguageId } from 'vs/editor/common/services/getIconClasses'; import * as nls from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; @@ -115,7 +116,8 @@ async function computePicks(snippetService: ISnippetsService, userDataProfileSer label: languageId, description: `(${label})`, filepath: joinPath(dir, `${languageId}.json`), - hint: true + hint: true, + iconClasses: getIconClassesForLanguageId(languageId) }); } } @@ -225,10 +227,10 @@ export class ConfigureSnippetsAction extends SnippetsAction { constructor() { super({ id: 'workbench.action.openSnippets', - title: nls.localize2('openSnippet.label', "Configure User Snippets"), + title: nls.localize2('openSnippet.label', "Configure Snippets"), shortTitle: { - ...nls.localize2('userSnippets', "User Snippets"), - mnemonicTitle: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets"), + ...nls.localize2('userSnippets', "Snippets"), + mnemonicTitle: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "&&Snippets"), }, f1: true, menu: [ diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts index 82e14ccc1a6..6b5bef90c2a 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SnippetFile, Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { URI } from 'vs/base/common/uri'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts index 8c185e766a5..d4824fdf3b9 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { getNonWhitespacePrefix } from 'vs/workbench/contrib/snippets/browser/snippetsService'; import { Position } from 'vs/editor/common/core/position'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsRewrite.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsRewrite.test.ts index 5ac2018ef7d..39b490f2fe3 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsRewrite.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsRewrite.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { generateUuid } from 'vs/base/common/uuid'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index ce61496ec51..297836af6c8 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SnippetCompletion, SnippetCompletionProvider } from 'vs/workbench/contrib/snippets/browser/snippetCompletionProvider'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index b0efa674be4..0306b35c7d4 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -153,7 +153,7 @@ export class SpeechService extends Disposable implements ISpeechService { const disposables = new DisposableStore(); const onSessionStoppedOrCanceled = () => { - this.activeSpeechToTextSessions--; + this.activeSpeechToTextSessions = Math.max(0, this.activeSpeechToTextSessions - 1); if (!this.hasActiveSpeechToTextSession) { this.speechToTextInProgress.reset(); } @@ -264,7 +264,7 @@ export class SpeechService extends Disposable implements ISpeechService { const disposables = new DisposableStore(); const onSessionStoppedOrCanceled = (dispose: boolean) => { - this.activeTextToSpeechSessions--; + this.activeTextToSpeechSessions = Math.max(0, this.activeTextToSpeechSessions - 1); if (!this.hasActiveTextToSpeechSession) { this.textToSpeechInProgress.reset(); } @@ -406,7 +406,7 @@ export class SpeechService extends Disposable implements ISpeechService { const disposables = new DisposableStore(); const onSessionStoppedOrCanceled = () => { - this.activeKeywordRecognitionSessions--; + this.activeKeywordRecognitionSessions = Math.max(0, this.activeKeywordRecognitionSessions - 1); this._onDidEndKeywordRecognition.fire(); disposables.dispose(); diff --git a/src/vs/workbench/contrib/speech/test/common/speechService.test.ts b/src/vs/workbench/contrib/speech/test/common/speechService.test.ts index d757eace7e0..16a4f0d9b77 100644 --- a/src/vs/workbench/contrib/speech/test/common/speechService.test.ts +++ b/src/vs/workbench/contrib/speech/test/common/speechService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { speechLanguageConfigToLanguage } from 'vs/workbench/contrib/speech/common/speechService'; diff --git a/src/vs/workbench/contrib/splash/browser/partsSplash.ts b/src/vs/workbench/contrib/splash/browser/partsSplash.ts index 6982a2cd50b..8b2dd0dcdd1 100644 --- a/src/vs/workbench/contrib/splash/browser/partsSplash.ts +++ b/src/vs/workbench/contrib/splash/browser/partsSplash.ts @@ -110,8 +110,6 @@ export class PartsSplash { // remove initial colors const defaultStyles = mainWindow.document.head.getElementsByClassName('initialShellColors'); - if (defaultStyles.length) { - mainWindow.document.head.removeChild(defaultStyles[0]); - } + defaultStyles[0]?.remove(); } } diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts index b560ed8999b..1fd7dd05886 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts @@ -178,7 +178,59 @@ const ModulesToLookFor = [ '@azure/web-pubsub-express', '@azure/openai', '@azure/arm-hybridkubernetes', - '@azure/arm-kubernetesconfiguration' + '@azure/arm-kubernetesconfiguration', + //AI and vector db dev packages + '@anthropic-ai/sdk', + '@anthropic-ai/tokenizer', + '@arizeai/openinference-instrumentation-langchain', + '@arizeai/openinference-instrumentation-openai', + '@aws-sdk-client-bedrock-runtime', + '@aws-sdk/client-bedrock', + '@datastax/astra-db-ts', + 'fireworks-js', + '@google-cloud/aiplatform', + '@huggingface/inference', + 'humanloop', + '@langchain/anthropic', + 'langsmith', + 'llamaindex', + 'mongodb', + 'neo4j-driver', + 'ollama', + 'onnxruntime-node', + 'onnxruntime-web', + 'pg', + 'postgresql', + 'redis', + '@supabase/supabase-js', + '@tensorflow/tfjs', + '@xenova/transformers', + 'tika', + 'weaviate-client', + '@zilliz/milvus2-sdk-node', + //Azure AI + '@azure-rest/ai-anomaly-detector', + '@azure-rest/ai-content-safety', + '@azure-rest/ai-document-intelligence', + '@azure-rest/ai-document-translator', + '@azure-rest/ai-personalizer', + '@azure-rest/ai-translation-text', + '@azure-rest/ai-vision-image-analysis', + '@azure/ai-anomaly-detector', + '@azure/ai-form-recognizer', + '@azure/ai-language-conversations', + '@azure/ai-language-text', + '@azure/ai-text-analytics', + '@azure/arm-botservice', + '@azure/arm-cognitiveservices', + '@azure/arm-machinelearning', + '@azure/cognitiveservices-contentmoderator', + '@azure/cognitiveservices-customvision-prediction', + '@azure/cognitiveservices-customvision-training', + '@azure/cognitiveservices-face', + '@azure/cognitiveservices-translatortext', + 'microsoft-cognitiveservices-speech-sdk' + ]; const PyMetaModulesToLookFor = [ @@ -311,7 +363,38 @@ const PyModulesToLookFor = [ 'guidance', 'openai', 'semantic-kernel', - 'sentence-transformers' + 'sentence-transformers', + // AI and vector db dev packages + 'anthropic', + 'aporia', + 'arize', + 'deepchecks', + 'fireworks-ai', + 'langchain-fireworks', + 'humanloop', + 'pymongo', + 'langchain-anthropic', + 'langchain-huggingface', + 'langchain-fireworks', + 'ollama', + 'onnxruntime', + 'pgvector', + 'sentence-transformers', + 'tika', + 'trulens', + 'trulens-eval', + 'wandb', + // Azure AI Services + 'azure-ai-contentsafety', + 'azure-ai-documentintelligence', + 'azure-ai-translation-text', + 'azure-ai-vision', + 'azure-cognitiveservices-language-luis', + 'azure-cognitiveservices-speech', + 'azure-cognitiveservices-vision-contentmoderator', + 'azure-cognitiveservices-vision-face', + 'azure-mgmt-cognitiveservices', + 'azure-mgmt-search' ]; const GoModulesToLookFor = [ @@ -428,6 +511,12 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.react" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@angular/core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.vue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@anthropic-ai/sdk" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@anthropic-ai/tokenizer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@arizeai/openinference-instrumentation-langchain" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@arizeai/openinference-instrumentation-openai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@aws-sdk-client-bedrock-runtime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@aws-sdk/client-bedrock" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.aws-sdk" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.aws-amplify-sdk" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -439,11 +528,13 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.@azure/keyvault" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/search" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/storage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@google-cloud/aiplatform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.azure" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.azure-storage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@google-cloud/common" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.firebase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.heroku-cli" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@huggingface/inference" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@microsoft/teams-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@microsoft/office-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@microsoft/office-js-helpers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -470,15 +561,35 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.cypress" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.chroma" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.faiss" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.fireworks-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@datastax/astra-db-ts" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.humanloop" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.langchain" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@langchain/anthropic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.langsmith" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.llamaindex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.milvus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.mongodb" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.neo4j-driver" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.ollama" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.onnxruntime-node" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.onnxruntime-web" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.openai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.pinecone" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.postgresql" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.pg" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.qdrant" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.redis" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@supabase/supabase-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@tensorflow/tfjs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@xenova/transformers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.weaviate-client" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@zilliz/milvus2-sdk-node" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.nightwatch" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.protractor" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.puppeteer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.selenium-webdriver" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.tika" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.webdriverio" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.gherkin" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/app-configuration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -490,6 +601,27 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.@azure/synapse-artifacts" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/synapse-access-control" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/ai-metrics-advisor" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-anomaly-detector" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-content-safety" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-document-intelligence" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-document-translator" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-personalizer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-translation-text" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure-rest/ai-vision-image-analysis" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/ai-anomaly-detector" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/ai-form-recognizer" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/ai-language-conversations" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/ai-language-text" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/ai-text-analytics" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/arm-botservice" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/arm-cognitiveservices" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/arm-machinelearning" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/cognitiveservices-contentmoderator" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/cognitiveservices-customvision-prediction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/cognitiveservices-customvision-training" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/cognitiveservices-face" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@azure/cognitiveservices-translatortext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.microsoft-cognitiveservices-speech-sdk" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/service-bus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/keyvault-secrets" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@azure/keyvault-keys" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -630,6 +762,16 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.azure-ai-language-conversations" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-ai-language-questionanswering" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-ai-ml" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-ai-contentsafety" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-ai-documentintelligence" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-ai-translation-text" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-ai-vision" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-cognitiveservices-language-luis" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-cognitiveservices-speech" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-cognitiveservices-vision-contentmoderator" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-cognitiveservices-vision-face" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-mgmt-cognitiveservices" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-mgmt-search" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-ai-translation-document" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-cognitiveservices" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -745,13 +887,30 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.azure-messaging-webpubsubservice" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-data-nspkg" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-data-tables" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.arize" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.aporia" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.anthropic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.deepchecks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.fireworks-ai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.transformers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.humanloop" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.langchain" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.langchain-anthropic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.langchain-fireworks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.langchain-huggingface" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.llama-index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.guidance" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.ollama" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.onnxruntime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.openai" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.pymongo" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.pgvector" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.semantic-kernel" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.sentence-transformers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.tika" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.trulens" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.trulens-eval" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.wandb" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.go.mod.github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, diff --git a/src/vs/workbench/contrib/tags/test/node/workspaceTags.test.ts b/src/vs/workbench/contrib/tags/test/node/workspaceTags.test.ts index 13993422b73..d166a151900 100644 --- a/src/vs/workbench/contrib/tags/test/node/workspaceTags.test.ts +++ b/src/vs/workbench/contrib/tags/test/node/workspaceTags.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as crypto from 'crypto'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { getHashedRemotesFromConfig as baseGetHashedRemotesFromConfig } from 'vs/workbench/contrib/tags/common/workspaceTags'; diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 0e9b7879ba0..e11e8a81fc3 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -3241,7 +3241,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer let configFileCreated = false; this._fileService.stat(resource).then((stat) => stat, () => undefined).then(async (stat) => { const fileExists: boolean = !!stat; - const configValue = this._configurationService.inspect('tasks'); + const configValue = this._configurationService.inspect('tasks', { resource }); let tasksExistInFile: boolean; let target: ConfigurationTarget; switch (taskSource) { diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 08e8292b60c..88059901e64 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -23,7 +23,7 @@ import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/se import { ITaskEvent, TaskEventKind, TaskGroup, TaskSettingId, TASKS_CATEGORY, TASK_RUNNING_STATE } from 'vs/workbench/contrib/tasks/common/tasks'; import { ITaskService, TaskCommandsRegistered, TaskExecutionSupportedContext } from 'vs/workbench/contrib/tasks/common/taskService'; -import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { RunAutomaticTasks, ManageAutomaticTaskRunning } from 'vs/workbench/contrib/tasks/browser/runAutomaticTasks'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; @@ -109,7 +109,7 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench } if (promise && (event.kind === TaskEventKind.Active) && (this._activeTasksCount === 1)) { - this._progressService.withProgress({ location: ProgressLocation.Window, command: 'workbench.action.tasks.showTasks', type: 'loading' }, progress => { + this._progressService.withProgress({ location: ProgressLocation.Window, command: 'workbench.action.tasks.showTasks' }, progress => { progress.report({ message: nls.localize('building', 'Building...') }); return promise!; }).then(() => { @@ -347,7 +347,7 @@ class UserTasksGlobalActionContribution extends Disposable implements IWorkbench private registerActions() { const id = 'workbench.action.tasks.openUserTasks'; - const title = nls.localize('userTasks', "User Tasks"); + const title = nls.localize('tasks', "Tasks"); this._register(MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { command: { id, @@ -431,15 +431,24 @@ schema.oneOf = [...(schemaVersion2.oneOf || []), ...(schemaVersion1.oneOf || []) const jsonRegistry = Registry.as(jsonContributionRegistry.Extensions.JSONContribution); jsonRegistry.registerSchema(tasksSchemaId, schema); -ProblemMatcherRegistry.onMatcherChanged(() => { - updateProblemMatchers(); - jsonRegistry.notifySchemaChanged(tasksSchemaId); -}); +export class TaskRegistryContribution extends Disposable implements IWorkbenchContribution { + static ID = 'taskRegistryContribution'; + constructor() { + super(); + + this._register(ProblemMatcherRegistry.onMatcherChanged(() => { + updateProblemMatchers(); + jsonRegistry.notifySchemaChanged(tasksSchemaId); + })); + + this._register(TaskDefinitionRegistry.onDefinitionsChanged(() => { + updateTaskDefinitions(); + jsonRegistry.notifySchemaChanged(tasksSchemaId); + })); + } +} +registerWorkbenchContribution2(TaskRegistryContribution.ID, TaskRegistryContribution, WorkbenchPhase.AfterRestored); -TaskDefinitionRegistry.onDefinitionsChanged(() => { - updateTaskDefinitions(); - jsonRegistry.notifySchemaChanged(tasksSchemaId); -}); const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts b/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts index 6b41d88c5f7..eba74599af7 100644 --- a/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as matchers from 'vs/workbench/contrib/tasks/common/problemMatcher'; -import * as assert from 'assert'; +import assert from 'assert'; import { ValidationState, IProblemReporter, ValidationStatus } from 'vs/base/common/parsers'; class ProblemReporter implements IProblemReporter { diff --git a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts index 816894f6b32..7aad1e55307 100644 --- a/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import * as assert from 'assert'; +import assert from 'assert'; import Severity from 'vs/base/common/severity'; import * as UUID from 'vs/base/common/uuid'; diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 774ff81af37..65b2301495f 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -31,8 +31,9 @@ import { mainWindow } from 'vs/base/browser/window'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { isBoolean, isNumber, isString } from 'vs/base/common/types'; import { LayoutSettings } from 'vs/workbench/services/layout/browser/layoutService'; -import { AutoUpdateConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; +import { AutoRestartConfigurationKey, AutoUpdateConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { KEYWORD_ACTIVIATION_SETTING_ID } from 'vs/workbench/contrib/chat/common/chatService'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; type TelemetryData = { mimeType: TelemetryTrustedValue; @@ -240,6 +241,7 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc constructor( @IConfigurationService private readonly configurationService: IConfigurationService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); @@ -419,6 +421,32 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'source of the setting' }; }>('window.systemColorTheme', { settingValue: this.getValueToReport(key, target), source }); return; + + case 'window.newWindowProfile': + { + const valueToReport = this.getValueToReport(key, target); + const settingValue = + valueToReport === null ? 'null' + : valueToReport === this.userDataProfilesService.defaultProfile.name + ? 'default' + : 'custom'; + this.telemetryService.publicLog2('window.newWindowProfile', { settingValue, source }); + return; + } + + case AutoRestartConfigurationKey: + this.telemetryService.publicLog2('extensions.autoRestart', { settingValue: this.getValueToReport(key, target), source }); + return; } } diff --git a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts index e355b287f63..606202e8837 100644 --- a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts +++ b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts @@ -39,7 +39,7 @@ export class EnvironmentVariableInfoStale implements IEnvironmentVariableInfo { private _getActions(): ITerminalStatusHoverAction[] { return [{ - label: localize('relaunchTerminalLabel', "Relaunch terminal"), + label: localize('relaunchTerminalLabel', "Relaunch Terminal"), run: () => this._terminalService.getInstanceFromId(this._terminalId)?.relaunch(), commandId: TerminalCommandId.Relaunch }]; @@ -77,7 +77,7 @@ export class EnvironmentVariableInfoChangesActive implements IEnvironmentVariabl private _getActions(scope: EnvironmentVariableScope | undefined): ITerminalStatusHoverAction[] { return [{ - label: localize('showEnvironmentContributions', "Show environment contributions"), + label: localize('showEnvironmentContributions', "Show Environment Contributions"), run: () => this._commandService.executeCommand(TerminalCommandId.ShowEnvironmentContributions, scope), commandId: TerminalCommandId.ShowEnvironmentContributions }]; diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index a1637a25529..06036fcbaae 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -112,18 +112,26 @@ __vsc_escape_value() { fi # Process text byte by byte, not by codepoint. - builtin local LC_ALL=C str="${1}" i byte token out='' + local -r LC_ALL=C + local -r str="${1}" + local -ir len="${#str}" + + local -i i + local -i val + local byte + local token + local out='' for (( i=0; i < "${#str}"; ++i )); do + # Escape backslashes, semi-colons specially, then special ASCII chars below space (0x20). byte="${str:$i:1}" - - # Escape backslashes, semi-colons specially, then special ASCII chars below space (0x20) - if [ "$byte" = "\\" ]; then + builtin printf -v val '%d' "'$byte" + if (( val < 31 )); then + builtin printf -v token '\\x%02x' "'$byte" + elif (( val == 92 )); then # \ token="\\\\" - elif [ "$byte" = ";" ]; then + elif (( val == 59 )); then # ; token="\\x3b" - elif (( $(builtin printf '%d' "'$byte") < 31 )); then - token=$(builtin printf '\\x%02x' "'$byte") else token="$byte" fi @@ -131,7 +139,7 @@ __vsc_escape_value() { out+="$token" done - builtin printf '%s\n' "${out}" + builtin printf '%s\n' "$out" } # Send the IsWindows property if the environment looks like Windows @@ -162,13 +170,19 @@ __vsc_current_command="" __vsc_nonce="$VSCODE_NONCE" unset VSCODE_NONCE +# Some features should only work in Insiders +__vsc_stable="$VSCODE_STABLE" +unset VSCODE_STABLE + # Report continuation prompt -builtin printf "\e]633;P;ContinuationPrompt=$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')\a" +if [ "$__vsc_stable" = "0" ]; then + builtin printf "\e]633;P;ContinuationPrompt=$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')\a" +fi __vsc_report_prompt() { # Expand the original PS1 similarly to how bash would normally # See https://stackoverflow.com/a/37137981 for technique - if ((BASH_VERSINFO[0] >= 4)); then + if ((BASH_VERSINFO[0] >= 5 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4))); then __vsc_prompt=${__vsc_original_PS1@P} else __vsc_prompt=${__vsc_original_PS1} @@ -244,7 +258,10 @@ __vsc_update_prompt() { __vsc_precmd() { __vsc_command_complete "$__vsc_status" __vsc_current_command="" - __vsc_report_prompt + # Report prompt is a work in progress, currently encoding is too slow + if [ "$__vsc_stable" = "0" ]; then + __vsc_report_prompt + fi __vsc_first_prompt=1 __vsc_update_prompt } diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 index f010f5945cb..6c92130ec95 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 @@ -21,6 +21,9 @@ $Global:__LastHistoryId = -1 $Nonce = $env:VSCODE_NONCE $env:VSCODE_NONCE = $null +$isStable = $env:VSCODE_STABLE +$env:VSCODE_STABLE = $null + $osVersion = [System.Environment]::OSVersion.Version $isWindows10 = $IsWindows -and $osVersion.Major -eq 10 -and $osVersion.Minor -eq 0 -and $osVersion.Build -lt 22000 @@ -52,7 +55,7 @@ if ($env:VSCODE_ENV_APPEND) { function Global:__VSCode-Escape-Value([string]$value) { # NOTE: In PowerShell v6.1+, this can be written `$value -replace '…', { … }` instead of `[regex]::Replace`. # Replace any non-alphanumeric characters. - [regex]::Replace($value, "[$([char]0x1b)\\\n;]", { param($match) + [regex]::Replace($value, "[$([char]0x00)-$([char]0x1f)\\\n;]", { param($match) # Encode the (ascii) matches as `\x` -Join ( [System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ } @@ -95,7 +98,9 @@ function Global:Prompt() { # Prompt # OSC 633 ; = ST - $Result += "$([char]0x1b)]633;P;Prompt=$(__VSCode-Escape-Value $OriginalPrompt)`a" + if ($isStable -eq "0") { + $Result += "$([char]0x1b)]633;P;Prompt=$(__VSCode-Escape-Value $OriginalPrompt)`a" + } # Write command started $Result += "$([char]0x1b)]633;B`a" @@ -142,9 +147,11 @@ else { } # Set ContinuationPrompt property -$ContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt -if ($ContinuationPrompt) { - [Console]::Write("$([char]0x1b)]633;P;ContinuationPrompt=$(__VSCode-Escape-Value $ContinuationPrompt)`a") +if ($isStable -eq "0") { + $ContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt + if ($ContinuationPrompt) { + [Console]::Write("$([char]0x1b)]633;P;ContinuationPrompt=$(__VSCode-Escape-Value $ContinuationPrompt)`a") + } } # Set always on key handlers which map to default VS Code keybindings @@ -169,8 +176,9 @@ function Set-MappedKeyHandlers { Set-MappedKeyHandler -Chord Shift+Enter -Sequence 'F12,c' Set-MappedKeyHandler -Chord Shift+End -Sequence 'F12,d' - # Conditionally enable suggestions - if ($env:VSCODE_SUGGEST -eq '1') { + # Enable suggestions if the environment variable is set and Windows PowerShell is not being used + # as APIs are not available to support this feature + if ($env:VSCODE_SUGGEST -eq '1' -and $PSVersionTable.PSVersion -ge "6.0") { Remove-Item Env:VSCODE_SUGGEST # VS Code send completions request (may override Ctrl+Spacebar) @@ -203,11 +211,23 @@ function Send-Completions { # `[` is included here as namespace commands are not included in CompleteCommand(''), # additionally for some reason CompleteVariable('[') causes the prompt to clear and reprint # multiple times - if ($completionPrefix.Contains(' ') -or $completionPrefix.Contains('[')) { + if ($completionPrefix.Contains(' ') -or $completionPrefix.Contains('[') -or $PSVersionTable.PSVersion -lt "6.0") { $completions = TabExpansion2 -inputScript $completionPrefix -cursorColumn $cursorIndex if ($null -ne $completions.CompletionMatches) { $result += ";$($completions.ReplacementIndex);$($completions.ReplacementLength);$($cursorIndex);" - $result += $completions.CompletionMatches | ConvertTo-Json -Compress + if ($completions.CompletionMatches.Count -gt 0 -and $completions.CompletionMatches.Where({ $_.ResultType -eq 3 -or $_.ResultType -eq 4 })) { + $json = [System.Collections.ArrayList]@($completions.CompletionMatches) + # Add . and .. to the completions list + $json.Add([System.Management.Automation.CompletionResult]::new( + '.', '.', [System.Management.Automation.CompletionResultType]::ProviderContainer, (Get-Location).Path) + ) + $json.Add([System.Management.Automation.CompletionResult]::new( + '..', '..', [System.Management.Automation.CompletionResultType]::ProviderContainer, (Split-Path (Get-Location) -Parent)) + ) + $result += $json | ConvertTo-Json -Compress + } else { + $result += $completions.CompletionMatches | ConvertTo-Json -Compress + } } } # If there is no space, get completions using CompletionCompleters as it gives us more diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index c3ee3fb42d7..b531e31130f 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -73,8 +73,8 @@ } .monaco-workbench .xterm { - /* All terminals have at least 10px left/right edge padding and 2 padding on the bottom (so underscores on last line are visible */ - padding: 0 10px 2px; + /* All terminals have at least 20px left, 10px right edge padding and 2 padding on the bottom (so underscores on last line are visible) */ + padding: 0 10px 2px 20px; } .monaco-workbench .terminal-editor .xterm, @@ -130,15 +130,6 @@ .xterm.xterm-cursor-pointer .xterm-screen { cursor: pointer; } .xterm.column-select.focus .xterm-screen { cursor: crosshair; } -.monaco-workbench .terminal-editor .xterm { - padding-left: 20px !important; -} - -.monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:first-child .xterm, -.integrated-terminal.shell-integration .xterm { - padding-left: 20px !important; -} - .monaco-workbench .terminal-editor .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .xterm, .monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .xterm { padding-right: 20px; diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 96657abf2ac..3d07aa2d8bc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -210,7 +210,7 @@ registerSendSequenceKeybinding('\x1b[24~d', { // F12,d -> shift+end (SelectLine) mac: { primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.RightArrow } }); registerSendSequenceKeybinding('\x1b[24~e', { // F12,e -> ctrl+space (Native suggest) - when: ContextKeyExpr.and(TerminalContextKeys.focus, ContextKeyExpr.equals(TerminalContextKeyStrings.ShellType, WindowsShellType.PowerShell), TerminalContextKeys.terminalShellIntegrationEnabled, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate(), ContextKeyExpr.or(ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true), ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.EnabledLegacy}`, true))), + when: ContextKeyExpr.and(TerminalContextKeys.focus, ContextKeyExpr.equals(TerminalContextKeyStrings.ShellType, WindowsShellType.PowerShell), TerminalContextKeys.terminalShellIntegrationEnabled, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate(), ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true)), primary: KeyMod.CtrlCmd | KeyCode.Space, mac: { primary: KeyMod.WinCtrl | KeyCode.Space } }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index 466958b0319..57848596b73 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -177,7 +177,7 @@ class SplitPaneContainer extends Disposable { // Remove old split view while (this._container.children.length > 0) { - this._container.removeChild(this._container.children[0]); + this._container.children[0].remove(); } this._splitViewDisposables.clear(); this._splitView.dispose(); @@ -288,7 +288,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this._onPanelOrientationChanged.fire(this._terminalLocation === ViewContainerLocation.Panel && this._panelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL); this._register(toDisposable(() => { if (this._container && this._groupElement) { - this._container.removeChild(this._groupElement); + this._groupElement.remove(); this._groupElement = undefined; } })); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index e6ca453010d..a48841d9802 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -88,6 +88,7 @@ import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/commo import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { shouldPasteTerminalText } from 'vs/workbench/contrib/terminal/common/terminalClipboard'; import { TerminalIconPicker } from 'vs/workbench/contrib/terminal/browser/terminalIconPicker'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; // HACK: This file should not depend on terminalContrib // eslint-disable-next-line local/code-import-patterns @@ -702,7 +703,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const verticalPadding = parseInt(computedStyle.paddingTop) + parseInt(computedStyle.paddingBottom); TerminalInstance._lastKnownCanvasDimensions = new dom.Dimension( Math.min(Constants.MaxCanvasWidth, width - horizontalPadding), - height + (this._hasScrollBar && !this._horizontalScrollbar ? -5/* scroll bar height */ : 0) - 2/* bottom padding */ - verticalPadding); + height - verticalPadding + (this._hasScrollBar && this._horizontalScrollbar ? -5/* scroll bar height */ : 0)); return TerminalInstance._lastKnownCanvasDimensions; } @@ -857,7 +858,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Determine whether to send ETX (ctrl+c) before running the command. This should always // happen unless command detection can reliably say that a command is being entered and // there is no content in the prompt - if (commandDetection?.hasInput !== false) { + if (!commandDetection || commandDetection.promptInputModel.value.length > 0) { await this.sendText('\x03', false); // Wait a little before running the command to avoid the sequences being echoed while the ^C // is being evaluated @@ -1843,60 +1844,74 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - @debounce(50) - private async _resize(): Promise { - this._resizeNow(false); - } + private async _resize(immediate?: boolean): Promise { + if (!this.xterm) { + return; + } - private async _resizeNow(immediate: boolean): Promise { let cols = this.cols; let rows = this.rows; - if (this.xterm) { - // Only apply these settings when the terminal is visible so that - // the characters are measured correctly. - if (this._isVisible && this._layoutSettingsChanged) { - const font = this.xterm.getFont(); - const config = this._terminalConfigurationService.config; - this.xterm.raw.options.letterSpacing = font.letterSpacing; - this.xterm.raw.options.lineHeight = font.lineHeight; - this.xterm.raw.options.fontSize = font.fontSize; - this.xterm.raw.options.fontFamily = font.fontFamily; - this.xterm.raw.options.fontWeight = config.fontWeight; - this.xterm.raw.options.fontWeightBold = config.fontWeightBold; + // Only apply these settings when the terminal is visible so that + // the characters are measured correctly. + if (this._isVisible && this._layoutSettingsChanged) { + const font = this.xterm.getFont(); + const config = this._terminalConfigurationService.config; + this.xterm.raw.options.letterSpacing = font.letterSpacing; + this.xterm.raw.options.lineHeight = font.lineHeight; + this.xterm.raw.options.fontSize = font.fontSize; + this.xterm.raw.options.fontFamily = font.fontFamily; + this.xterm.raw.options.fontWeight = config.fontWeight; + this.xterm.raw.options.fontWeightBold = config.fontWeightBold; - // Any of the above setting changes could have changed the dimensions of the - // terminal, re-evaluate now. - this._initDimensions(); - cols = this.cols; - rows = this.rows; + // Any of the above setting changes could have changed the dimensions of the + // terminal, re-evaluate now. + this._initDimensions(); + cols = this.cols; + rows = this.rows; - this._layoutSettingsChanged = false; - } - - if (isNaN(cols) || isNaN(rows)) { - return; - } - - if (cols !== this.xterm.raw.cols || rows !== this.xterm.raw.rows) { - if (this._fixedRows || this._fixedCols) { - await this._updateProperty(ProcessPropertyType.FixedDimensions, { cols: this._fixedCols, rows: this._fixedRows }); - } - this._onDimensionsChanged.fire(); - } - - this.xterm.raw.resize(cols, rows); - TerminalInstance._lastKnownGridDimensions = { cols, rows }; + this._layoutSettingsChanged = false; } + if (isNaN(cols) || isNaN(rows)) { + return; + } + + if (cols !== this.xterm.raw.cols || rows !== this.xterm.raw.rows) { + if (this._fixedRows || this._fixedCols) { + await this._updateProperty(ProcessPropertyType.FixedDimensions, { cols: this._fixedCols, rows: this._fixedRows }); + } + this._onDimensionsChanged.fire(); + } + + TerminalInstance._lastKnownGridDimensions = { cols, rows }; + if (immediate) { - // do not await, call setDimensions synchronously - this._processManager.setDimensions(cols, rows, true); + this.xterm.raw.resize(cols, rows); + await this._updatePtyDimensions(this.xterm.raw); } else { - await this._processManager.setDimensions(cols, rows); + // Update dimensions independently as vertical resize is cheap but horizontal resize is + // expensive due to reflow. + this._resizeVertically(this.xterm.raw, rows); + this._resizeHorizontally(this.xterm.raw, cols); } } + private async _resizeVertically(rawXterm: XTermTerminal, rows: number): Promise { + rawXterm.resize(rawXterm.cols, rows); + await this._updatePtyDimensions(rawXterm); + } + + @debounce(50) + private async _resizeHorizontally(rawXterm: XTermTerminal, cols: number): Promise { + rawXterm.resize(cols, rawXterm.rows); + await this._updatePtyDimensions(rawXterm); + } + + private async _updatePtyDimensions(rawXterm: XTermTerminal): Promise { + await this._processManager.setDimensions(rawXterm.cols, rawXterm.rows); + } + setShellType(shellType: TerminalShellType | undefined) { if (this._shellType === shellType) { return; @@ -1976,7 +1991,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } this._dimensionsOverride = dimensions; if (immediate) { - this._resizeNow(true); + this._resize(true); } else { this._resize(); } @@ -2288,15 +2303,14 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID private readonly _container: HTMLElement, @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, + @IHostService private readonly _hostService: IHostService, ) { super(); this._register(toDisposable(() => this._clearDropOverlay())); } private _clearDropOverlay() { - if (this._dropOverlay && this._dropOverlay.parentElement) { - this._dropOverlay.parentElement.removeChild(this._dropOverlay); - } + this._dropOverlay?.remove(); this._dropOverlay = undefined; } @@ -2372,9 +2386,9 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID path = URI.file(JSON.parse(rawCodeFiles)[0]); } - if (!path && e.dataTransfer.files.length > 0 && e.dataTransfer.files[0].path /* Electron only */) { + if (!path && e.dataTransfer.files.length > 0 && this._hostService.getPathForFile(e.dataTransfer.files[0])) { // Check if the file was dragged from the filesystem - path = URI.file(e.dataTransfer.files[0].path); + path = URI.file(this._hostService.getPathForFile(e.dataTransfer.files[0])!); } if (!path) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 01db326f019..22204b7a88e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -289,7 +289,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const options: ITerminalProcessOptions = { shellIntegration: { enabled: this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled), - suggestEnabled: this._configurationService.getValue(TerminalSuggestSettingId.Enabled) || this._configurationService.getValue(TerminalSuggestSettingId.EnabledLegacy), + suggestEnabled: this._configurationService.getValue(TerminalSuggestSettingId.Enabled), nonce: this.shellIntegrationNonce }, windowsEnableConpty: this._terminalConfigurationService.config.windowsEnableConpty, @@ -489,7 +489,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const options: ITerminalProcessOptions = { shellIntegration: { enabled: this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled), - suggestEnabled: this._configurationService.getValue(TerminalSuggestSettingId.Enabled) || this._configurationService.getValue(TerminalSuggestSettingId.EnabledLegacy), + suggestEnabled: this._configurationService.getValue(TerminalSuggestSettingId.Enabled), nonce: this.shellIntegrationNonce }, windowsEnableConpty: this._terminalConfigurationService.config.windowsEnableConpty, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index 24dde378f30..a4b662a065c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -22,6 +22,7 @@ import { URI } from 'vs/base/common/uri'; import { deepClone } from 'vs/base/common/objects'; import { isUriComponents } from 'vs/platform/terminal/common/terminalProfiles'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { Disposable } from 'vs/base/common/lifecycle'; export interface IProfileContextProvider { getDefaultSystemShell(remoteAuthority: string | undefined, os: OperatingSystem): Promise; @@ -34,7 +35,7 @@ const generatedProfileName = 'Generated'; * Resolves terminal shell launch config and terminal profiles for the given operating system, * environment, and user configuration. */ -export abstract class BaseTerminalProfileResolverService implements ITerminalProfileResolverService { +export abstract class BaseTerminalProfileResolverService extends Disposable implements ITerminalProfileResolverService { declare _serviceBrand: undefined; private _primaryBackendOs: OperatingSystem | undefined; @@ -54,19 +55,21 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro private readonly _workspaceContextService: IWorkspaceContextService, private readonly _remoteAgentService: IRemoteAgentService ) { + super(); + if (this._remoteAgentService.getConnection()) { this._remoteAgentService.getEnvironment().then(env => this._primaryBackendOs = env?.os || OS); } else { this._primaryBackendOs = OS; } - this._configurationService.onDidChangeConfiguration(e => { + this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(TerminalSettingId.DefaultProfileWindows) || e.affectsConfiguration(TerminalSettingId.DefaultProfileMacOs) || e.affectsConfiguration(TerminalSettingId.DefaultProfileLinux)) { this._refreshDefaultProfileName(); } - }); - this._terminalProfileService.onDidChangeAvailableProfiles(() => this._refreshDefaultProfileName()); + })); + this._register(this._terminalProfileService.onDidChangeAvailableProfiles(() => this._refreshDefaultProfileName())); } @debounce(200) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts index eb3dc1c08f3..ce5bf14c23c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts @@ -71,7 +71,7 @@ export class TerminalProfileService extends Disposable implements ITerminalProfi // in web, we don't want to show the dropdown unless there's a web extension // that contributes a profile - this._extensionService.onDidChangeExtensions(() => this.refreshAvailableProfiles()); + this._register(this._extensionService.onDidChangeExtensions(() => this.refreshAvailableProfiles())); this._webExtensionContributedProfileContextKey = TerminalContextKeys.webExtensionContributedProfile.bindTo(this._contextKeyService); this._updateWebContextKey(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts index a17b660a13c..b795f91f201 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts @@ -269,6 +269,9 @@ export async function showRunRecentQuickPick( return; } const [item] = quickPick.activeItems; + if (!item) { + return; + } if ('command' in item && item.command && item.command.marker) { if (!terminalScrollStateSaved) { xterm.markTracker.saveScrollState(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index af9508e1b1f..e2edac02afd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -285,24 +285,26 @@ export class TerminalService extends Disposable implements ITerminalService { const isPersistentRemote = !!this._environmentService.remoteAuthority && enableTerminalReconnection; - this._primaryBackend?.onDidRequestDetach(async (e) => { - const instanceToDetach = this.getInstanceFromResource(getTerminalUri(e.workspaceId, e.instanceId)); - if (instanceToDetach) { - const persistentProcessId = instanceToDetach?.persistentProcessId; - if (persistentProcessId && !instanceToDetach.shellLaunchConfig.isFeatureTerminal && !instanceToDetach.shellLaunchConfig.customPtyImplementation) { - if (instanceToDetach.target === TerminalLocation.Editor) { - this._terminalEditorService.detachInstance(instanceToDetach); + if (this._primaryBackend) { + this._register(this._primaryBackend.onDidRequestDetach(async (e) => { + const instanceToDetach = this.getInstanceFromResource(getTerminalUri(e.workspaceId, e.instanceId)); + if (instanceToDetach) { + const persistentProcessId = instanceToDetach?.persistentProcessId; + if (persistentProcessId && !instanceToDetach.shellLaunchConfig.isFeatureTerminal && !instanceToDetach.shellLaunchConfig.customPtyImplementation) { + if (instanceToDetach.target === TerminalLocation.Editor) { + this._terminalEditorService.detachInstance(instanceToDetach); + } else { + this._terminalGroupService.getGroupForInstance(instanceToDetach)?.removeInstance(instanceToDetach); + } + await instanceToDetach.detachProcessAndDispose(TerminalExitReason.User); + await this._primaryBackend?.acceptDetachInstanceReply(e.requestId, persistentProcessId); } else { - this._terminalGroupService.getGroupForInstance(instanceToDetach)?.removeInstance(instanceToDetach); + // will get rejected without a persistentProcessId to attach to + await this._primaryBackend?.acceptDetachInstanceReply(e.requestId, undefined); } - await instanceToDetach.detachProcessAndDispose(TerminalExitReason.User); - await this._primaryBackend?.acceptDetachInstanceReply(e.requestId, persistentProcessId); - } else { - // will get rejected without a persistentProcessId to attach to - await this._primaryBackend?.acceptDetachInstanceReply(e.requestId, undefined); } - } - }); + })); + } mark('code/terminal/willReconnect'); let reconnectedPromise: Promise; @@ -335,16 +337,16 @@ export class TerminalService extends Disposable implements ITerminalService { } private _forwardInstanceHostEvents(host: ITerminalInstanceHost) { - host.onDidChangeInstances(this._onDidChangeInstances.fire, this._onDidChangeInstances); - host.onDidDisposeInstance(this._onDidDisposeInstance.fire, this._onDidDisposeInstance); - host.onDidChangeActiveInstance(instance => this._evaluateActiveInstance(host, instance)); - host.onDidFocusInstance(instance => { + this._register(host.onDidChangeInstances(this._onDidChangeInstances.fire, this._onDidChangeInstances)); + this._register(host.onDidDisposeInstance(this._onDidDisposeInstance.fire, this._onDidDisposeInstance)); + this._register(host.onDidChangeActiveInstance(instance => this._evaluateActiveInstance(host, instance))); + this._register(host.onDidFocusInstance(instance => { this._onDidFocusInstance.fire(instance); this._evaluateActiveInstance(host, instance); - }); - host.onDidChangeInstanceCapability((instance) => { + })); + this._register(host.onDidChangeInstanceCapability((instance) => { this._onDidChangeInstanceCapability.fire(instance); - }); + })); this._hostActiveTerminals.set(host, undefined); } @@ -1207,7 +1209,7 @@ class TerminalEditorStyle extends Themable { super(_themeService); this._registerListeners(); this._styleElement = dom.createStyleSheet(container); - this._register(toDisposable(() => container.removeChild(this._styleElement))); + this._register(toDisposable(() => this._styleElement.remove())); this.updateStyles(); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index a93fccec1b5..285c7a72ec1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -174,9 +174,7 @@ export class TerminalTabbedView extends Disposable { } else { if (this._splitView.length === 2 && !this._terminalTabsMouseContextKey.get()) { this._splitView.removeView(this._tabTreeIndex); - if (this._plusButton) { - this._tabContainer.removeChild(this._plusButton); - } + this._plusButton?.remove(); this._removeSashListener(); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index b32bd3f28d6..b81f20a1e7d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -51,6 +51,7 @@ import { Schemas } from 'vs/base/common/network'; import { getColorForSeverity } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { TerminalContextActionRunner } from 'vs/workbench/contrib/terminal/browser/terminalContextMenu'; import type { IHoverAction } from 'vs/base/browser/ui/hover/hover'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; const $ = DOM.$; @@ -577,6 +578,7 @@ class TerminalTabsDragAndDrop extends Disposable implements IListDragAndDrop 0 && e.dataTransfer.files[0].path /* Electron only */) { + if (!resource && e.dataTransfer.files.length > 0 && this._hostService.getPathForFile(e.dataTransfer.files[0])) { // Check if the file was dragged from the filesystem - resource = URI.file(e.dataTransfer.files[0].path); + resource = URI.file(this._hostService.getPathForFile(e.dataTransfer.files[0])!); } if (!resource) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 966c4ead234..3cf3c286a93 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -585,7 +585,7 @@ class TerminalThemeIconStyle extends Themable { super(_themeService); this._registerListeners(); this._styleElement = dom.createStyleSheet(container); - this._register(toDisposable(() => container.removeChild(this._styleElement))); + this._register(toDisposable(() => this._styleElement.remove())); this.updateStyles(); } diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts b/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts index 032610dbea7..63283a6a76b 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/widgetManager.ts @@ -19,8 +19,8 @@ export class TerminalWidgetManager implements IDisposable { } dispose(): void { - if (this._container && this._container.parentElement) { - this._container.parentElement.removeChild(this._container); + if (this._container) { + this._container.remove(); this._container = undefined; } } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 67136657ac5..8d14cc3a245 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -9,6 +9,7 @@ import type { Unicode11Addon as Unicode11AddonType } from '@xterm/addon-unicode1 import type { WebglAddon as WebglAddonType } from '@xterm/addon-webgl'; import type { SerializeAddon as SerializeAddonType } from '@xterm/addon-serialize'; import type { ImageAddon as ImageAddonType } from '@xterm/addon-image'; +import type { ClipboardAddon as ClipboardAddonType, ClipboardSelectionType } from '@xterm/addon-clipboard'; import * as dom from 'vs/base/browser/dom'; import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -44,6 +45,7 @@ const enum RenderConstants { SmoothScrollDuration = 125 } +let ClipboardAddon: typeof ClipboardAddonType; let ImageAddon: typeof ImageAddonType; let SearchAddon: typeof SearchAddonType; let SerializeAddon: typeof SerializeAddonType; @@ -118,6 +120,9 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach private _shellIntegrationAddon: ShellIntegrationAddon; private _decorationAddon: DecorationAddon; + // Always on dynamicly imported addons + private _clipboardAddon?: ClipboardAddonType; + // Optional addons private _searchAddon?: SearchAddonType; private _unicode11Addon?: Unicode11AddonType; @@ -273,6 +278,17 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this.raw.loadAddon(this._decorationAddon); this._shellIntegrationAddon = new ShellIntegrationAddon(shellIntegrationNonce, disableShellIntegrationReporting, this._telemetryService, this._logService); this.raw.loadAddon(this._shellIntegrationAddon); + this._getClipboardAddonConstructor().then(ClipboardAddon => { + this._clipboardAddon = this._instantiationService.createInstance(ClipboardAddon, undefined, { + async readText(type: ClipboardSelectionType): Promise { + return _clipboardService.readText(type === 'p' ? 'selection' : 'clipboard'); + }, + async writeText(type: ClipboardSelectionType, text: string): Promise { + return _clipboardService.writeText(text, type === 'p' ? 'selection' : 'clipboard'); + } + }); + this.raw.loadAddon(this._clipboardAddon); + }); this._anyTerminalFocusContextKey = TerminalContextKeys.focusInAny.bindTo(contextKeyService); this._anyFocusedTerminalHasSelection = TerminalContextKeys.textSelectedInFocused.bindTo(contextKeyService); @@ -325,7 +341,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this.raw.open(container); } - // TODO: Move before open to the DOM renderer doesn't initialize + // TODO: Move before open so the DOM renderer doesn't initialize if (options.enableGpu) { if (this._shouldLoadWebgl()) { this._enableWebglRenderer(); @@ -710,6 +726,13 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach } } + protected async _getClipboardAddonConstructor(): Promise { + if (!ClipboardAddon) { + ClipboardAddon = (await importAMDNodeModule('@xterm/addon-clipboard', 'lib/addon-clipboard.js')).ClipboardAddon; + } + return ClipboardAddon; + } + protected async _getImageAddonConstructor(): Promise { if (!ImageAddon) { ImageAddon = (await importAMDNodeModule('@xterm/addon-image', 'lib/addon-image.js')).ImageAddon; diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 406bfbb41cf..81ccf23927f 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; -import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; +import { IProcessEnvironment, isLinux, OperatingSystem } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; @@ -33,7 +33,12 @@ export const TERMINAL_CONFIG_SECTION = 'terminal.integrated'; export const DEFAULT_LETTER_SPACING = 0; export const MINIMUM_LETTER_SPACING = -5; -export const DEFAULT_LINE_HEIGHT = 1; +// HACK: On Linux it's common for fonts to include an underline that is rendered lower than the +// bottom of the cell which causes it to be cut off due to `overflow:hidden` in the DOM renderer. +// See: +// - https://github.com/microsoft/vscode/issues/211933 +// - https://github.com/xtermjs/xterm.js/issues/4067 +export const DEFAULT_LINE_HEIGHT = isLinux ? 1.1 : 1; export const MINIMUM_FONT_WEIGHT = 1; export const MAXIMUM_FONT_WEIGHT = 1000; @@ -204,8 +209,6 @@ export interface ITerminalConfiguration { shellIntegration?: { enabled: boolean; decorationsEnabled: boolean; - // TODO: Legacy - remove soon - suggestEnabled: boolean; }; enableImages: boolean; smoothScrolling: boolean; diff --git a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts index ced2afffa11..86a5a037b4d 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts @@ -23,12 +23,7 @@ export const TERMINAL_FOREGROUND_COLOR = registerColor('terminal.foreground', { }, nls.localize('terminal.foreground', 'The foreground color of the terminal.')); export const TERMINAL_CURSOR_FOREGROUND_COLOR = registerColor('terminalCursor.foreground', null, nls.localize('terminalCursor.foreground', 'The foreground color of the terminal cursor.')); export const TERMINAL_CURSOR_BACKGROUND_COLOR = registerColor('terminalCursor.background', null, nls.localize('terminalCursor.background', 'The background color of the terminal cursor. Allows customizing the color of a character overlapped by a block cursor.')); -export const TERMINAL_SELECTION_BACKGROUND_COLOR = registerColor('terminal.selectionBackground', { - light: editorSelectionBackground, - dark: editorSelectionBackground, - hcDark: editorSelectionBackground, - hcLight: editorSelectionBackground -}, nls.localize('terminal.selectionBackground', 'The selection background color of the terminal.')); +export const TERMINAL_SELECTION_BACKGROUND_COLOR = registerColor('terminal.selectionBackground', editorSelectionBackground, nls.localize('terminal.selectionBackground', 'The selection background color of the terminal.')); export const TERMINAL_INACTIVE_SELECTION_BACKGROUND_COLOR = registerColor('terminal.inactiveSelectionBackground', { light: transparent(TERMINAL_SELECTION_BACKGROUND_COLOR, 0.5), dark: transparent(TERMINAL_SELECTION_BACKGROUND_COLOR, 0.5), @@ -59,18 +54,8 @@ export const TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR = registerColor( hcDark: '#F14C4C', hcLight: '#B5200D' }, nls.localize('terminalCommandDecoration.errorBackground', 'The terminal command decoration background color for error commands.')); -export const TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR = registerColor('terminalOverviewRuler.cursorForeground', { - dark: '#A0A0A0CC', - light: '#A0A0A0CC', - hcDark: '#A0A0A0CC', - hcLight: '#A0A0A0CC' -}, nls.localize('terminalOverviewRuler.cursorForeground', 'The overview ruler cursor color.')); -export const TERMINAL_BORDER_COLOR = registerColor('terminal.border', { - dark: PANEL_BORDER, - light: PANEL_BORDER, - hcDark: PANEL_BORDER, - hcLight: PANEL_BORDER -}, nls.localize('terminal.border', 'The color of the border that separates split panes within the terminal. This defaults to panel.border.')); +export const TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR = registerColor('terminalOverviewRuler.cursorForeground', '#A0A0A0CC', nls.localize('terminalOverviewRuler.cursorForeground', 'The overview ruler cursor color.')); +export const TERMINAL_BORDER_COLOR = registerColor('terminal.border', PANEL_BORDER, nls.localize('terminal.border', 'The color of the border that separates split panes within the terminal. This defaults to panel.border.')); export const TERMINAL_FIND_MATCH_BACKGROUND_COLOR = registerColor('terminal.findMatchBackground', { dark: editorFindMatch, light: editorFindMatch, @@ -78,12 +63,7 @@ export const TERMINAL_FIND_MATCH_BACKGROUND_COLOR = registerColor('terminal.find hcDark: null, hcLight: '#0F4A85' }, nls.localize('terminal.findMatchBackground', 'Color of the current search match in the terminal. The color must not be opaque so as not to hide underlying terminal content.'), true); -export const TERMINAL_HOVER_HIGHLIGHT_BACKGROUND_COLOR = registerColor('terminal.hoverHighlightBackground', { - dark: transparent(editorHoverHighlight, 0.5), - light: transparent(editorHoverHighlight, 0.5), - hcDark: transparent(editorHoverHighlight, 0.5), - hcLight: transparent(editorHoverHighlight, 0.5) -}, nls.localize('terminal.findMatchHighlightBorder', 'Border color of the other search matches in the terminal.')); +export const TERMINAL_HOVER_HIGHLIGHT_BACKGROUND_COLOR = registerColor('terminal.hoverHighlightBackground', transparent(editorHoverHighlight, 0.5), nls.localize('terminal.findMatchHighlightBorder', 'Border color of the other search matches in the terminal.')); export const TERMINAL_FIND_MATCH_BORDER_COLOR = registerColor('terminal.findMatchBorder', { dark: null, light: null, @@ -108,18 +88,14 @@ export const TERMINAL_OVERVIEW_RULER_FIND_MATCH_FOREGROUND_COLOR = registerColor hcDark: '#f38518', hcLight: '#0F4A85' }, nls.localize('terminalOverviewRuler.findMatchHighlightForeground', 'Overview ruler marker color for find matches in the terminal.')); -export const TERMINAL_DRAG_AND_DROP_BACKGROUND = registerColor('terminal.dropBackground', { - dark: EDITOR_DRAG_AND_DROP_BACKGROUND, - light: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcDark: EDITOR_DRAG_AND_DROP_BACKGROUND, - hcLight: EDITOR_DRAG_AND_DROP_BACKGROUND -}, nls.localize('terminal.dragAndDropBackground', "Background color when dragging on top of terminals. The color should have transparency so that the terminal contents can still shine through."), true); -export const TERMINAL_TAB_ACTIVE_BORDER = registerColor('terminal.tab.activeBorder', { - dark: TAB_ACTIVE_BORDER, - light: TAB_ACTIVE_BORDER, - hcDark: TAB_ACTIVE_BORDER, - hcLight: TAB_ACTIVE_BORDER -}, nls.localize('terminal.tab.activeBorder', 'Border on the side of the terminal tab in the panel. This defaults to tab.activeBorder.')); +export const TERMINAL_DRAG_AND_DROP_BACKGROUND = registerColor('terminal.dropBackground', EDITOR_DRAG_AND_DROP_BACKGROUND, nls.localize('terminal.dragAndDropBackground', "Background color when dragging on top of terminals. The color should have transparency so that the terminal contents can still shine through."), true); +export const TERMINAL_TAB_ACTIVE_BORDER = registerColor('terminal.tab.activeBorder', TAB_ACTIVE_BORDER, nls.localize('terminal.tab.activeBorder', 'Border on the side of the terminal tab in the panel. This defaults to tab.activeBorder.')); +export const TERMINAL_INITIAL_HINT_FOREGROUND = registerColor('terminal.initialHintForeground', { + dark: '#ffffff56', + light: '#0007', + hcDark: null, + hcLight: null +}, nls.localize('terminalInitialHintForeground', 'Foreground color of the terminal initial hint.')); export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefaults } } = { 'terminal.ansiBlack': { diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigurationService.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigurationService.test.ts index 1e2da27f7d3..9564cb8df6a 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigurationService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigurationService.test.ts @@ -6,6 +6,7 @@ import { notStrictEqual, strictEqual } from 'assert'; import { getActiveWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { isLinux } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -219,7 +220,7 @@ suite('Workbench - TerminalConfigurationService', () => { } } }); - strictEqual(terminalConfigurationService.getFont(getActiveWindow()).lineHeight, 1, 'editor.lineHeight should be 1 when terminal.integrated.lineHeight not set'); + strictEqual(terminalConfigurationService.getFont(getActiveWindow()).lineHeight, isLinux ? 1.1 : 1, 'editor.lineHeight should be the default when terminal.integrated.lineHeight not set'); }); }); diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts index 5ea55926c39..a15a395d9d8 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Extensions as ThemeingExtensions, IColorRegistry, ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { ansiColorIdentifiers, registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts index dd74c586060..5aa97b16452 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts index ef6c5b46e76..a547d3f68ae 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { importAMDNodeModule } from 'vs/amdX'; import { isWindows } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css index ba2c96cf4a9..c2f0de22f7d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css @@ -3,9 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .pane-body.integrated-terminal .contentWidgets .terminal-initial-hint { - color: var(--vscode-input-placeholderForeground); +.monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint { + color: var(--vscode-terminal-initialHintForeground); } .monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint a { - color: var(--vscode-textLink-foreground) + cursor: pointer; +} + +.monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint a, +.monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint .detail { + font-style: italic; } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts index a2c974a0baa..ba1325e8af4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts @@ -18,6 +18,8 @@ registerTerminalContribution(TerminalChatController.ID, TerminalChatController, AccessibleViewRegistry.register(new TerminalInlineChatAccessibleView()); AccessibleViewRegistry.register(new TerminalChatAccessibilityHelp()); +registerWorkbenchContribution2(TerminalChatEnabler.Id, TerminalChatEnabler, WorkbenchPhase.AfterRestored); + // #endregion // #region Actions @@ -25,5 +27,7 @@ AccessibleViewRegistry.register(new TerminalChatAccessibilityHelp()); import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { TerminalChatAccessibilityHelp } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; +import { registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; +import { TerminalChatEnabler } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler'; // #endregion diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts index dbead33ec9a..21d5f887145 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts @@ -29,9 +29,16 @@ import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal import 'vs/css!./media/terminalInitialHint'; import { TerminalInitialHintSettingId } from 'vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration'; import { ChatAgentLocation, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; const $ = dom.$; +const enum Constants { + InitialHintHideStorageKey = 'terminal.initialHint.hide' +} + export class InitialHintAddon extends Disposable implements ITerminalAddon { private readonly _onDidRequestCreateHint = this._register(new Emitter()); get onDidRequestCreateHint(): Event { return this._onDidRequestCreateHint.event; } @@ -90,11 +97,22 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IStorageService private readonly _storageService: IStorageService, ) { super(); + + // Reset hint state when config changes + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TerminalInitialHintSettingId.Enabled)) { + this._storageService.remove(Constants.InitialHintHideStorageKey, StorageScope.APPLICATION); + } + })); } xtermOpen(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { + if (this._storageService.getBoolean(Constants.InitialHintHideStorageKey, StorageScope.APPLICATION, false)) { + return; + } if (this._terminalGroupService.instances.length + this._terminalEditorService.instances.length !== 1) { // only show for the first terminal return; @@ -108,7 +126,7 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm private _createHint(): void { const instance = this._instance instanceof TerminalInstance ? this._instance : undefined; const commandDetectionCapability = instance?.capabilities.get(TerminalCapability.CommandDetection); - if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || commandDetectionCapability?.hasInput || instance.reconnectionProperties) { + if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || commandDetectionCapability.promptInputModel.value || !!instance.shellLaunchConfig.attachPersistentProcess) { return; } @@ -130,19 +148,24 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm marker, x: this._xterm.raw.buffer.active.cursorX + 1, }); + if (this._decoration) { + this._register(this._decoration); + } } - this._register(this._xterm.raw.onKey(() => { - this._decoration?.dispose(); - this._addon?.dispose(); + this._register(this._xterm.raw.onKey(() => this.dispose())); + + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TerminalInitialHintSettingId.Enabled) && !this._configurationService.getValue(TerminalInitialHintSettingId.Enabled)) { + this.dispose(); + } })); const inputModel = commandDetectionCapability.promptInputModel; if (inputModel) { this._register(inputModel.onDidChangeInput(() => { if (inputModel.value) { - this._decoration?.dispose(); - this._addon?.dispose(); + this.dispose(); } })); } @@ -181,8 +204,6 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm } registerTerminalContribution(TerminalInitialHintContribution.ID, TerminalInitialHintContribution, false); - - class TerminalInitialHintWidget extends Disposable { @@ -193,12 +214,15 @@ class TerminalInitialHintWidget extends Disposable { constructor( private readonly _instance: ITerminalInstance, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, @IKeybindingService private readonly keybindingService: IKeybindingService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService private readonly productService: IProductService, - @ITerminalService private readonly terminalService: ITerminalService + @ITerminalService private readonly terminalService: ITerminalService, + @IStorageService private readonly _storageService: IStorageService, + @IContextMenuService private readonly contextMenuService: IContextMenuService ) { super(); this.toDispose.add(_instance.onDidFocus(() => { @@ -219,11 +243,16 @@ class TerminalInitialHintWidget extends Disposable { } private _getHintInlineChat(agents: IChatAgent[]) { - const providerName = (agents.length === 1 ? agents[0].fullName : undefined) ?? this.productService.nameShort; + let providerName = (agents.length === 1 ? agents[0].fullName : undefined) ?? this.productService.nameShort; + const defaultAgent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); + if (defaultAgent?.extensionId.value === agents[0].extensionId.value) { + providerName = defaultAgent.fullName ?? providerName; + } let ariaLabel = `Ask ${providerName} something or start typing to dismiss.`; const handleClick = () => { + this._storageService.store(Constants.InitialHintHideStorageKey, true, StorageScope.APPLICATION, StorageTarget.USER); this.telemetryService.publicLog2('workbenchActionExecuted', { id: 'terminalInlineChat.hintAction', from: 'hint' @@ -232,6 +261,7 @@ class TerminalInitialHintWidget extends Disposable { }; this.toDispose.add(this.commandService.onDidExecuteCommand(e => { if (e.commandId === TerminalChatCommandId.Start) { + this._storageService.store(Constants.InitialHintHideStorageKey, true, StorageScope.APPLICATION, StorageTarget.USER); this.dispose(); } })); @@ -247,7 +277,7 @@ class TerminalInitialHintWidget extends Disposable { } }; - const hintElement = $('terminal-initial-hint'); + const hintElement = $('div.terminal-initial-hint'); hintElement.style.display = 'block'; const keybindingHint = this.keybindingService.lookupKeybinding(TerminalChatCommandId.Start); @@ -275,8 +305,7 @@ class TerminalInitialHintWidget extends Disposable { hintElement.appendChild(after); const typeToDismiss = localize('hintTextDismiss', 'Start typing to dismiss.'); - const textHint2 = $('span', undefined, typeToDismiss); - textHint2.style.fontStyle = 'italic'; + const textHint2 = $('span.detail', undefined, typeToDismiss); hintElement.appendChild(textHint2); ariaLabel = actionPart.concat(typeToDismiss); @@ -308,8 +337,23 @@ class TerminalInitialHintWidget extends Disposable { this.domNode = undefined; })); + this.toDispose.add(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, (e) => { + this.contextMenuService.showContextMenu({ + getAnchor: () => { return new StandardMouseEvent(dom.getActiveWindow(), e); }, + getActions: () => { + return [{ + id: 'workench.action.disableTerminalInitialHint', + label: localize('disableInitialHint', "Disable Initial Hint"), + tooltip: localize('disableInitialHint', "Disable Initial Hint"), + enabled: true, + class: undefined, + run: () => this.configurationService.updateValue(TerminalInitialHintSettingId.Enabled, false) + } + ]; + } + }); + })); } - return this.domNode; } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index bf95499ee32..13d70f86352 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -15,9 +15,6 @@ export const enum TerminalChatCommandId { Discard = 'workbench.action.terminal.chat.discard', MakeRequest = 'workbench.action.terminal.chat.makeRequest', Cancel = 'workbench.action.terminal.chat.cancel', - FeedbackHelpful = 'workbench.action.terminal.chat.feedbackHelpful', - FeedbackUnhelpful = 'workbench.action.terminal.chat.feedbackUnhelpful', - FeedbackReportIssue = 'workbench.action.terminal.chat.feedbackReportIssue', RunCommand = 'workbench.action.terminal.chat.runCommand', RunFirstCommand = 'workbench.action.terminal.chat.runFirstCommand', InsertCommand = 'workbench.action.terminal.chat.insertCommand', @@ -30,7 +27,6 @@ export const enum TerminalChatCommandId { export const MENU_TERMINAL_CHAT_INPUT = MenuId.for('terminalChatInput'); export const MENU_TERMINAL_CHAT_WIDGET = MenuId.for('terminalChatWidget'); export const MENU_TERMINAL_CHAT_WIDGET_STATUS = MenuId.for('terminalChatWidget.status'); -export const MENU_TERMINAL_CHAT_WIDGET_FEEDBACK = MenuId.for('terminalChatWidget.feedback'); export const MENU_TERMINAL_CHAT_WIDGET_TOOLBAR = MenuId.for('terminalChatWidget.toolbar'); export const enum TerminalChatContextKeyStrings { @@ -61,18 +57,12 @@ export namespace TerminalChatContextKeys { /** Whether the chat input has text */ export const inputHasText = new RawContextKey(TerminalChatContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text.")); - /** Whether the terminal chat agent has been registered */ - export const agentRegistered = new RawContextKey(TerminalChatContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered.")); - /** The chat response contains at least one code block */ export const responseContainsCodeBlock = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseContainsCodeBlock, false, localize('chatResponseContainsCodeBlockContextKey', "Whether the chat response contains a code block.")); /** The chat response contains multiple code blocks */ export const responseContainsMultipleCodeBlocks = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseContainsMultipleCodeBlocks, false, localize('chatResponseContainsMultipleCodeBlocksContextKey', "Whether the chat response contains multiple code blocks.")); - /** Whether the response supports issue reporting */ - export const responseSupportsIssueReporting = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting")); - - /** The chat vote, if any for the response, if any */ - export const sessionResponseVote = new RawContextKey(TerminalChatContextKeyStrings.ChatSessionResponseVote, undefined, { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); + /** A chat agent exists for the terminal location */ + export const hasChatAgent = new RawContextKey(TerminalChatContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether a chat agent is registered for the terminal location.")); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index f0bdac465ba..35fcdd0410c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -35,6 +35,7 @@ export class TerminalChatAccessibilityHelp implements IAccessibleViewImplentatio options: { type: AccessibleViewType.Help } }; } + dispose() { } } export function getAccessibilityHelpText(accessor: ServicesAccessor): string { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts index 215f1fe6f05..2898dfde41c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -33,4 +33,5 @@ export class TerminalInlineChatAccessibleView implements IAccessibleViewImplenta options: { type: AccessibleViewType.View } }; } + dispose() { } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index e8ce9449b40..d53b7cd4968 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -9,11 +9,11 @@ import { localize2 } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AbstractInlineChatAction } from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; -import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; registerActiveXtermAction({ @@ -29,8 +29,7 @@ registerActiveXtermAction({ category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - // TODO: This needs to change to check for a terminal location capable agent - CTX_INLINE_CHAT_HAS_PROVIDER + TerminalChatContextKeys.hasChatAgent ), run: (_xterm, _accessor, activeInstance, opts?: unknown) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -164,7 +163,6 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.responseContainsMultipleCodeBlocks.negate() ), @@ -196,7 +194,6 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, TerminalChatContextKeys.responseContainsMultipleCodeBlocks ), icon: Codicon.play, @@ -227,7 +224,6 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.responseContainsMultipleCodeBlocks.negate() ), @@ -259,7 +255,6 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, TerminalChatContextKeys.responseContainsMultipleCodeBlocks ), keybinding: { @@ -289,14 +284,13 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, ), icon: Codicon.commentDiscussion, menu: [{ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock.negate(), TerminalChatContextKeys.requestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock, TerminalChatContextKeys.requestActive.negate()), }, { id: MENU_TERMINAL_CHAT_WIDGET, @@ -319,12 +313,11 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.agentRegistered, CTX_INLINE_CHAT_EMPTY.negate() ), icon: Codicon.send, keybinding: { - when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalChatContextKeys.requestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.focused, TerminalChatContextKeys.requestActive.negate()), weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Enter }, @@ -348,7 +341,6 @@ registerActiveXtermAction({ title: localize2('cancelChat', 'Cancel Chat'), precondition: ContextKeyExpr.and( TerminalChatContextKeys.requestActive, - TerminalChatContextKeys.agentRegistered ), icon: Codicon.debugStop, menu: { @@ -366,25 +358,39 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalChatCommandId.FeedbackReportIssue, - title: localize2('reportIssue', 'Report Issue'), - precondition: ContextKeyExpr.and( - TerminalChatContextKeys.requestActive.negate(), - TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), - TerminalChatContextKeys.responseSupportsIssueReporting - ), - icon: Codicon.report, - menu: [{ - id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), TerminalChatContextKeys.responseSupportsIssueReporting), - group: 'inline', - order: 3 - }], + id: TerminalChatCommandId.PreviousFromHistory, + title: localize2('previousFromHitory', 'Previous From History'), + precondition: TerminalChatContextKeys.focused, + keybinding: { + when: TerminalChatContextKeys.focused, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.UpArrow, + }, + run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { return; } const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - contr?.acceptFeedback(); + contr?.populateHistory(true); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.NextFromHistory, + title: localize2('nextFromHitory', 'Next From History'), + precondition: TerminalChatContextKeys.focused, + keybinding: { + when: TerminalChatContextKeys.focused, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.DownArrow, + }, + + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.populateHistory(false); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 5734afb1b87..005fdf6ca7e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -4,26 +4,27 @@ *--------------------------------------------------------------------------------------------*/ import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { GeneratingPhrase, IChatAccessibilityService, IChatCodeBlockContextProviderService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentLocation, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatUserAction, IChatProgress, IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatCodeBlockContextProviderService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { ChatModel, ChatRequestModel, IChatRequestVariableData, IChatResponseModel, getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { DeferredPromise } from 'vs/base/common/async'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { assertType } from 'vs/base/common/types'; +import { CancelablePromise, createCancelablePromise, DeferredPromise } from 'vs/base/common/async'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; const enum Message { NONE = 0, @@ -48,6 +49,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr */ static activeChatWidget?: TerminalChatController; + private static _storageKey = 'terminal-inline-chat-history'; + private static _promptHistory: string[] = []; + /** * The chat widget for the controller, this is lazy as we don't want to instantiate it until * both it's required and xterm is ready. @@ -61,17 +65,11 @@ export class TerminalChatController extends Disposable implements ITerminalContr get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } private readonly _requestActiveContextKey: IContextKey; - private readonly _terminalAgentRegisteredContextKey: IContextKey; private readonly _responseContainsCodeBlockContextKey: IContextKey; private readonly _responseContainsMulitpleCodeBlocksContextKey: IContextKey; - private readonly _responseSupportsIssueReportingContextKey: IContextKey; - private readonly _sessionResponseVoteContextKey: IContextKey; private _messages = this._store.add(new Emitter()); - private _currentRequest: ChatRequestModel | undefined; - - private _lastInput: string | undefined; private _lastResponseContent: string | undefined; get lastResponseContent(): string | undefined { return this._lastResponseContent; @@ -81,7 +79,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr get onDidHide() { return this.chatWidget?.onDidHide ?? Event.None; } private _terminalAgentName = 'terminal'; - private _terminalAgentId: string | undefined; private readonly _model: MutableDisposable = this._register(new MutableDisposable()); @@ -89,31 +86,32 @@ export class TerminalChatController extends Disposable implements ITerminalContr return this._chatWidget?.value.inlineChatWidget.scopedContextKeyService ?? this._contextKeyService; } + private _sessionCtor: CancelablePromise | undefined; + private _historyOffset: number = -1; + private _historyCandidate: string = ''; + private _historyUpdate: (prompt: string) => void; + + private _currentRequestId: string | undefined; + private _activeRequestCts?: CancellationTokenSource; + constructor( private readonly _instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, @ITerminalService private readonly _terminalService: ITerminalService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatService private readonly _chatService: IChatService, @IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService, @IViewsService private readonly _viewsService: IViewsService, + @IStorageService private readonly _storageService: IStorageService, ) { super(); this._requestActiveContextKey = TerminalChatContextKeys.requestActive.bindTo(this._contextKeyService); - this._terminalAgentRegisteredContextKey = TerminalChatContextKeys.agentRegistered.bindTo(this._contextKeyService); this._responseContainsCodeBlockContextKey = TerminalChatContextKeys.responseContainsCodeBlock.bindTo(this._contextKeyService); this._responseContainsMulitpleCodeBlocksContextKey = TerminalChatContextKeys.responseContainsMultipleCodeBlocks.bindTo(this._contextKeyService); - this._responseSupportsIssueReportingContextKey = TerminalChatContextKeys.responseSupportsIssueReporting.bindTo(this._contextKeyService); - this._sessionResponseVoteContextKey = TerminalChatContextKeys.sessionResponseVote.bindTo(this._contextKeyService); - if (!this.initTerminalAgent()) { - this._register(this._chatAgentService.onDidChangeAgents(() => this.initTerminalAgent())); - } this._register(this._chatCodeBlockContextProviderService.registerProvider({ getCodeBlockContext: (editor) => { if (!editor || !this._chatWidget?.hasValue || !this.hasFocus()) { @@ -128,34 +126,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr } }, 'terminal')); - // TODO - // This is glue/debt that's needed while ChatModel isn't yet adopted. The chat model uses - // a default chat model (unless configured) and feedback is reported against that one. This - // code forwards the feedback to an actual registered provider - this._register(this._chatService.onDidPerformUserAction(e => { - // only forward feedback from the inline chat widget default model - if ( - this._chatWidget?.rawValue?.inlineChatWidget.usesDefaultChatModel - && e.sessionId === this._chatWidget?.rawValue?.inlineChatWidget.getChatModel().sessionId - ) { - if (e.action.kind === 'bug') { - this.acceptFeedback(undefined); - } else if (e.action.kind === 'vote') { - this.acceptFeedback(e.action.direction === ChatAgentVoteDirection.Up); - } + TerminalChatController._promptHistory = JSON.parse(this._storageService.get(TerminalChatController._storageKey, StorageScope.PROFILE, '[]')); + this._historyUpdate = (prompt: string) => { + const idx = TerminalChatController._promptHistory.indexOf(prompt); + if (idx >= 0) { + TerminalChatController._promptHistory.splice(idx, 1); } - })); - } - - private initTerminalAgent(): boolean { - const terminalAgent = this._chatAgentService.getAgentsByName(this._terminalAgentName)[0]; - if (terminalAgent) { - this._terminalAgentId = terminalAgent.id; - this._terminalAgentRegisteredContextKey.set(true); - return true; - } - - return false; + TerminalChatController._promptHistory.unshift(prompt); + this._historyOffset = -1; + this._historyCandidate = ''; + this._storageService.store(TerminalChatController._storageKey, JSON.stringify(TerminalChatController._promptHistory), StorageScope.PROFILE, StorageTarget.USER); + }; } xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { @@ -178,41 +159,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr }); } - acceptFeedback(helpful?: boolean): void { - const model = this._model.value; - if (!this._currentRequest || !model) { - return; - } - let action: ChatUserAction; - if (helpful === undefined) { - action = { kind: 'bug' }; - } else { - this._sessionResponseVoteContextKey.set(helpful ? 'up' : 'down'); - action = { kind: 'vote', direction: helpful ? ChatAgentVoteDirection.Up : ChatAgentVoteDirection.Down }; - } - // TODO:extract into helper method - for (const request of model.getRequests()) { - if (request.response?.response.value || request.response?.result) { - this._chatService.notifyUserAction({ - sessionId: request.session.sessionId, - requestId: request.id, - agentId: request.response?.agent?.id, - result: request.response?.result, - action - }); - } - } - this._chatWidget?.value.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); - } + private async _createSession(): Promise { + this._sessionCtor = createCancelablePromise(async token => { + if (!this._model.value) { + this._model.value = this._chatService.startSession(ChatAgentLocation.Terminal, token); - cancel(): void { - if (this._currentRequest) { - this._model.value?.cancelRequest(this._currentRequest); - } - this._requestActiveContextKey.set(false); - this._chatWidget?.value.inlineChatWidget.updateProgress(false); - this._chatWidget?.value.inlineChatWidget.updateInfo(''); - this._chatWidget?.value.inlineChatWidget.updateToolbar(true); + if (!this._model.value) { + throw new Error('Failed to start chat session'); + } + } + }); + this._register(toDisposable(() => this._sessionCtor?.cancel())); } private _forcedPlaceholder: string | undefined = undefined; @@ -239,112 +196,63 @@ export class TerminalChatController extends Disposable implements ITerminalContr } clear(): void { - if (this._currentRequest) { - this._model.value?.cancelRequest(this._currentRequest); - } + this.cancel(); this._model.clear(); - this._chatWidget?.rawValue?.hide(); - this._chatWidget?.rawValue?.setValue(undefined); this._responseContainsCodeBlockContextKey.reset(); - this._sessionResponseVoteContextKey.reset(); this._requestActiveContextKey.reset(); + this._chatWidget?.value.hide(); + this._chatWidget?.value.setValue(undefined); } async acceptInput(): Promise { - if (!this._model.value) { - this._model.value = this._chatService.startSession(ChatAgentLocation.Terminal, CancellationToken.None); - if (!this._model.value) { - throw new Error('Could not start chat session'); - } - } - this._messages.fire(Message.ACCEPT_INPUT); - const model = this._model.value; - - this._lastInput = this._chatWidget?.value?.input(); - if (!this._lastInput) { + assertType(this._chatWidget); + assertType(this._model.value); + const lastInput = this._chatWidget.value.inlineChatWidget.value; + if (!lastInput) { return; } - - const responseCreated = new DeferredPromise(); - let responseCreatedComplete = false; - const completeResponseCreated = () => { - if (!responseCreatedComplete && this._currentRequest?.response) { - responseCreated.complete(this._currentRequest.response); - responseCreatedComplete = true; - } - }; - - const accessibilityRequestId = this._chatAccessibilityService.acceptRequest(); + const model = this._model.value; + this._chatWidget.value.inlineChatWidget.setChatModel(model); + this._historyUpdate(lastInput); + this._activeRequestCts?.cancel(); + this._activeRequestCts = new CancellationTokenSource(); + const store = new DisposableStore(); this._requestActiveContextKey.set(true); - const cancellationToken = new CancellationTokenSource().token; let responseContent = ''; - const progressCallback = (progress: IChatProgress) => { - if (cancellationToken.isCancellationRequested) { - return; - } - - if (progress.kind === 'markdownContent') { - responseContent += progress.content.value; - } - if (this._currentRequest) { - model.acceptResponseProgress(this._currentRequest, progress); - completeResponseCreated(); - } - }; - - await model.waitForInitialization(); - this._chatWidget?.value.addToHistory(this._lastInput); - const request: IParsedChatRequest = { - text: this._lastInput, - parts: [] - }; - const requestVarData: IChatRequestVariableData = { - variables: [] - }; - this._currentRequest = model.addRequest(request, requestVarData, 0); - completeResponseCreated(); - const requestProps: IChatAgentRequest = { - sessionId: model.sessionId, - requestId: this._currentRequest!.id, - agentId: this._terminalAgentId!, - message: this._lastInput, - variables: { variables: [] }, - location: ChatAgentLocation.Terminal - }; + const response = await this._chatWidget.value.inlineChatWidget.chatWidget.acceptInput(lastInput); + this._currentRequestId = response?.requestId; + const responsePromise = new DeferredPromise(); try { - const task = this._chatAgentService.invokeAgent(this._terminalAgentId!, requestProps, progressCallback, getHistoryEntriesFromModel(model, this._terminalAgentId!), cancellationToken); - this._chatWidget?.value.inlineChatWidget.updateChatMessage(undefined); - this._chatWidget?.value.inlineChatWidget.updateProgress(true); - this._chatWidget?.value.inlineChatWidget.updateInfo(GeneratingPhrase + '\u2026'); - await task; - } catch (e) { - + this._requestActiveContextKey.set(true); + if (response) { + store.add(response.onDidChange(async () => { + responseContent += response.response.value; + if (response.isCanceled) { + this._requestActiveContextKey.set(false); + responsePromise.complete(undefined); + return; + } + if (response.isComplete) { + this._requestActiveContextKey.set(false); + this._requestActiveContextKey.set(false); + const containsCode = responseContent.includes('```'); + this._chatWidget!.value.inlineChatWidget.updateChatMessage({ message: new MarkdownString(responseContent), requestId: response!.requestId }, false, containsCode); + const firstCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(0); + const secondCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(1); + this._responseContainsCodeBlockContextKey.set(!!firstCodeBlock); + this._responseContainsMulitpleCodeBlocksContextKey.set(!!secondCodeBlock); + this._chatWidget?.value.inlineChatWidget.updateToolbar(true); + responsePromise.complete(response); + } + })); + } + await responsePromise.p; + return response; + } catch { + return; } finally { - this._requestActiveContextKey.set(false); - this._chatWidget?.value.inlineChatWidget.updateProgress(false); - this._chatWidget?.value.inlineChatWidget.updateInfo(''); - this._chatWidget?.value.inlineChatWidget.updateToolbar(true); - if (this._currentRequest) { - model.completeResponse(this._currentRequest); - completeResponseCreated(); - } - this._lastResponseContent = responseContent; - if (this._currentRequest) { - this._chatAccessibilityService.acceptResponse(responseContent, accessibilityRequestId); - const containsCode = responseContent.includes('```'); - this._chatWidget?.value.inlineChatWidget.updateChatMessage({ message: new MarkdownString(responseContent), requestId: this._currentRequest.id }, false, containsCode); - const firstCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(0); - const secondCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(1); - this._responseContainsCodeBlockContextKey.set(!!firstCodeBlock); - this._responseContainsMulitpleCodeBlocksContextKey.set(!!secondCodeBlock); - this._chatWidget?.value.inlineChatWidget.updateToolbar(true); - } - const supportIssueReporting = this._currentRequest?.response?.agent?.metadata?.supportIssueReporting; - if (supportIssueReporting !== undefined) { - this._responseSupportsIssueReportingContextKey.set(supportIssueReporting); - } + store.dispose(); } - return responseCreated.p; } updateInput(text: string, selectAll = true): void { @@ -369,6 +277,47 @@ export class TerminalChatController extends Disposable implements ITerminalContr return !!this._chatWidget?.rawValue?.hasFocus() ?? false; } + populateHistory(up: boolean) { + if (!this._chatWidget?.value) { + return; + } + + const len = TerminalChatController._promptHistory.length; + if (len === 0) { + return; + } + + if (this._historyOffset === -1) { + // remember the current value + this._historyCandidate = this._chatWidget.value.inlineChatWidget.value; + } + + const newIdx = this._historyOffset + (up ? 1 : -1); + if (newIdx >= len) { + // reached the end + return; + } + + let entry: string; + if (newIdx < 0) { + entry = this._historyCandidate; + this._historyOffset = -1; + } else { + entry = TerminalChatController._promptHistory[newIdx]; + this._historyOffset = newIdx; + } + + this._chatWidget.value.inlineChatWidget.value = entry; + this._chatWidget.value.inlineChatWidget.selectAll(); + } + + cancel(): void { + this._sessionCtor?.cancel(); + this._sessionCtor = undefined; + this._activeRequestCts?.cancel(); + this._requestActiveContextKey.set(false); + } + async acceptCommand(shouldExecute: boolean): Promise { const code = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(0); if (!code) { @@ -377,18 +326,22 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.value.acceptCommand(code.textEditorModel.getValue(), shouldExecute); } - reveal(): void { + async reveal(): Promise { + await this._createSession(); this._chatWidget?.value.reveal(); + this._chatWidget?.value.focus(); } async viewInChat(): Promise { + //TODO: is this necessary? better way? const widget = await showChatView(this._viewsService); - const request = this._currentRequest; - if (!widget || !request?.response) { + const currentRequest = this.chatWidget?.inlineChatWidget.chatWidget.viewModel?.model.getRequests().find(r => r.id === this._currentRequestId); + if (!widget || !currentRequest?.response) { return; } + const message: IChatProgress[] = []; - for (const item of request.response.response.value) { + for (const item of currentRequest.response.response.value) { if (item.kind === 'textEditGroup') { for (const group of item.edits) { message.push({ @@ -404,24 +357,15 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatService.addCompleteRequest(widget!.viewModel!.sessionId, // DEBT: Add hardcoded agent name until its removed - `@${this._terminalAgentName} ${request.message.text}`, - request.variableData, - request.attempt, + `@${this._terminalAgentName} ${currentRequest.message.text}`, + currentRequest.variableData, + currentRequest.attempt, { message, - result: request.response!.result, - followups: request.response!.followups + result: currentRequest.response!.result, + followups: currentRequest.response!.followups }); widget.focusLastMessage(); this._chatWidget?.rawValue?.hide(); } - - // TODO: Move to register calls, don't override - override dispose() { - if (this._currentRequest) { - this._model.value?.cancelRequest(this._currentRequest); - } - super.dispose(); - this.clear(); - } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts new file mode 100644 index 00000000000..fdcdf5c006f --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IChatAgentService, ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; + + +export class TerminalChatEnabler { + + static Id = 'terminalChat.enabler'; + + private readonly _ctxHasProvider: IContextKey; + + private readonly _store = new DisposableStore(); + + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @IChatAgentService chatAgentService: IChatAgentService + ) { + this._ctxHasProvider = TerminalChatContextKeys.hasChatAgent.bindTo(contextKeyService); + this._store.add(chatAgentService.onDidChangeAgents(() => { + const hasTerminalAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Terminal)); + this._ctxHasProvider.set(hasTerminalAgent); + })); + } + + dispose() { + this._ctxHasProvider.reset(); + this._store.dispose(); + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 8b9aedb6bc9..d28f3cf8455 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -13,14 +13,14 @@ import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { ITerminalInstance, type IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalStickyScrollContribution } from 'vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollContribution'; const enum Constants { - HorizontalMargin = 10 + HorizontalMargin = 10, + VerticalMargin = 30 } export class TerminalChatWidget extends Disposable { @@ -56,10 +56,14 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget = this._instantiationService.createInstance( InlineChatWidget, - ChatAgentLocation.Terminal, { - inputMenuId: MENU_TERMINAL_CHAT_INPUT, - widgetMenuId: MENU_TERMINAL_CHAT_WIDGET, + location: ChatAgentLocation.Terminal, + resolveData: () => { + // TODO@meganrogge return something that identifies this terminal + return undefined; + } + }, + { statusMenuId: { menu: MENU_TERMINAL_CHAT_WIDGET_STATUS, options: { @@ -72,14 +76,20 @@ export class TerminalChatWidget extends Disposable { } } }, - feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - telemetrySource: 'terminal-inline-chat', - rendererOptions: { editableCodeBlock: true } + chatWidgetViewOptions: { + rendererOptions: { editableCodeBlock: true }, + menus: { + telemetrySource: 'terminal-inline-chat', + executeToolbar: MENU_TERMINAL_CHAT_INPUT, + inputSideToolbar: MENU_TERMINAL_CHAT_WIDGET, + } + } } ); this._register(Event.any( this._inlineChatWidget.onDidChangeHeight, this._instance.onDimensionsChanged, + this._inlineChatWidget.chatWidget.onDidChangeContentHeight, Event.debounce(this._xterm.raw.onCursorMove, () => void 0, MicrotaskDelay), )(() => this._relayout())); @@ -91,11 +101,14 @@ export class TerminalChatWidget extends Disposable { this._container.appendChild(this._inlineChatWidget.domNode); this._focusTracker = this._register(trackFocus(this._container)); + this._register(this._focusTracker.onDidFocus(() => this._focusedContextKey.set(true))); this._register(this._focusTracker.onDidBlur(() => { + this._focusedContextKey.set(false); if (!this.inlineChatWidget.responseContent) { this.hide(); } })); + this.hide(); } @@ -115,15 +128,26 @@ export class TerminalChatWidget extends Disposable { const style = getActiveWindow().getComputedStyle(xtermElement); const xtermPadding = parseInt(style.paddingLeft) + parseInt(style.paddingRight); const width = Math.min(640, xtermElement.clientWidth - 12/* padding */ - 2/* border */ - Constants.HorizontalMargin - xtermPadding); - const height = Math.min(480, heightInPixel, this._getTerminalWrapperHeight() ?? Number.MAX_SAFE_INTEGER); + const terminalWrapperHeight = this._getTerminalWrapperHeight() ?? Number.MAX_SAFE_INTEGER; + let height = Math.min(480, heightInPixel, terminalWrapperHeight); + const top = this._getTop() ?? 0; if (width === 0 || height === 0) { return; } + + let adjustedHeight = undefined; + if (height < this._inlineChatWidget.contentHeight) { + if (height - top > 0) { + height = height - top - Constants.VerticalMargin; + } else { + height = height - Constants.VerticalMargin; + adjustedHeight = height; + } + } this._container.style.paddingLeft = style.paddingLeft; this._dimension = new Dimension(width, height); this._inlineChatWidget.layout(this._dimension); - - this._updateVerticalPosition(); + this._updateVerticalPosition(adjustedHeight); } private _reset() { @@ -134,13 +158,12 @@ export class TerminalChatWidget extends Disposable { reveal(): void { this._doLayout(this._inlineChatWidget.contentHeight); this._container.classList.remove('hide'); - this._focusedContextKey.set(true); this._visibleContextKey.set(true); this._inlineChatWidget.focus(); this._instance.scrollToBottom(); } - private _updateVerticalPosition(): void { + private _getTop(): number | undefined { const font = this._instance.xterm?.getFont(); if (!font?.charHeight) { return; @@ -149,14 +172,24 @@ export class TerminalChatWidget extends Disposable { const cellHeight = font.charHeight * font.lineHeight; const topPadding = terminalWrapperHeight - (this._instance.rows * cellHeight); const cursorY = (this._instance.xterm?.raw.buffer.active.cursorY ?? 0) + 1; - const top = topPadding + cursorY * cellHeight; + return topPadding + cursorY * cellHeight; + } + + private _updateVerticalPosition(adjustedHeight?: number): void { + const top = this._getTop(); + if (!top) { + return; + } this._container.style.top = `${top}px`; const widgetHeight = this._inlineChatWidget.contentHeight; + const terminalWrapperHeight = this._getTerminalWrapperHeight(); if (!terminalWrapperHeight) { return; } - if (top > terminalWrapperHeight - widgetHeight) { + if (top > terminalWrapperHeight - widgetHeight && terminalWrapperHeight - widgetHeight > 0) { this._setTerminalOffset(top - (terminalWrapperHeight - widgetHeight)); + } else if (adjustedHeight) { + this._setTerminalOffset(adjustedHeight); } else { this._setTerminalOffset(undefined); } @@ -168,12 +201,10 @@ export class TerminalChatWidget extends Disposable { hide(): void { this._container.classList.add('hide'); + this._inlineChatWidget.reset(); this._reset(); this._inlineChatWidget.updateChatMessage(undefined); - this._inlineChatWidget.updateProgress(false); this._inlineChatWidget.updateToolbar(false); - this._inlineChatWidget.reset(); - this._focusedContextKey.set(false); this._visibleContextKey.set(false); this._inlineChatWidget.value = ''; this._instance.focus(); @@ -213,10 +244,6 @@ export class TerminalChatWidget extends Disposable { this._instance.runCommand(code, shouldExecute); this.hide(); } - - updateProgress(progress?: IChatProgress): void { - this._inlineChatWidget.updateProgress(progress?.kind === 'markdownContent'); - } public get focusTracker(): IFocusTracker { return this._focusTracker; } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts index d3a1a8a0b11..0558b023b16 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts @@ -2,8 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// eslint-disable-next-line local/code-import-patterns, local/code-amd-node-module -import { Terminal } from '@xterm/xterm'; + +import type { Terminal } from '@xterm/xterm'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ShellIntegrationAddon } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; @@ -14,6 +14,7 @@ import { Emitter } from 'vs/base/common/event'; import { strictEqual } from 'assert'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ChatAgentLocation, IChatAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { importAMDNodeModule } from 'vs/amdX'; // Test TerminalInitialHintAddon @@ -46,9 +47,10 @@ suite('Terminal Initial Hint Addon', () => { locations: [ChatAgentLocation.fromRaw('editor')], invoke: async () => { return {}; } }; - setup(() => { + setup(async () => { const instantiationService = workbenchInstantiationService({}, store); - xterm = store.add(new Terminal()); + const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; + xterm = store.add(new TerminalCtor()); const shellIntegrationAddon = store.add(new ShellIntegrationAddon('', true, undefined, new NullLogService)); initialHintAddon = store.add(instantiationService.createInstance(InitialHintAddon, shellIntegrationAddon.capabilities, onDidChangeAgents)); store.add(initialHintAddon.onDidRequestCreateHint(() => eventCount++)); diff --git a/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts b/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts index 83836adc556..2321f8edb96 100644 --- a/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts @@ -114,7 +114,7 @@ registerTerminalAction({ text, name: text, ariaLabel: text, - showProgress: 'loading' + showProgress: true }; const statusbarHandle = statusbarService.addEntry(statusbarEntry, 'recordSession', StatusbarAlignment.LEFT); store.add(statusbarHandle); diff --git a/src/vs/workbench/contrib/terminalContrib/find/browser/textInputContextMenu.ts b/src/vs/workbench/contrib/terminalContrib/find/browser/textInputContextMenu.ts index 7253be4ca8e..d63dd7c113c 100644 --- a/src/vs/workbench/contrib/terminalContrib/find/browser/textInputContextMenu.ts +++ b/src/vs/workbench/contrib/terminalContrib/find/browser/textInputContextMenu.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from 'vs/base/browser/dom'; +import { getActiveWindow, isHTMLInputElement, isHTMLTextAreaElement } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { isNative } from 'vs/base/common/platform'; @@ -37,8 +37,8 @@ export function openContextMenu(targetWindow: Window, event: MouseEvent, clipboa else { const clipboardText = await clipboardService.readText(); if ( - element instanceof HTMLTextAreaElement || - element instanceof HTMLInputElement + isHTMLTextAreaElement(element) || + isHTMLInputElement(element) ) { const selectionStart = element.selectionStart || 0; const selectionEnd = element.selectionEnd || 0; diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts index d4247bbd835..f021a32d23e 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts @@ -106,11 +106,17 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { // Try extract any trailing line and column numbers by matching the text against parsed // links. This will give a search link `foo` on a line like `"foo", line 10` to open the // quick pick with `foo:10` as the contents. + // + // This also normalizes the path to remove suffixes like :10 or :5.0-4 if (link.contextLine) { const parsedLinks = detectLinks(link.contextLine, this._getOS()); - const matchingParsedLink = parsedLinks.find(parsedLink => parsedLink.suffix && link.text === parsedLink.path.text); + // Optimistically check that the link _starts with_ the parsed link text. If so, + // continue to use the parsed link + const matchingParsedLink = parsedLinks.find(parsedLink => parsedLink.suffix && link.text.startsWith(parsedLink.path.text)); if (matchingParsedLink) { if (matchingParsedLink.suffix?.row !== undefined) { + // Normalize the path based on the parsed link + text = matchingParsedLink.path.text; text += `:${matchingParsedLink.suffix.row}`; if (matchingParsedLink.suffix?.col !== undefined) { text += `:${matchingParsedLink.suffix.col}`; diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts index b419b7b94ca..80c28af6297 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import type { IBufferLine, IBufferCell } from '@xterm/xterm'; import { convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkHelpers'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css index bc8e99fb00f..9f88f2757db 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/media/stickyScroll.css @@ -11,6 +11,7 @@ z-index: 32; /* Must be higher than .xterm-viewport and decorations */ background: var(--vscode-terminalStickyScroll-background, var(--vscode-terminal-background, var(--vscode-panel-background))); box-shadow: var(--vscode-scrollbar-shadow) 0 3px 2px -2px; + border-bottom: 1px solid var(--vscode-terminalStickyScroll-border, transparent); } .part.sidebar .terminal-sticky-scroll, .part.auxiliarybar .terminal-sticky-scroll { diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry.ts index 5ab1af0d0eb..ed805826706 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry.ts @@ -3,20 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Color } from 'vs/base/common/color'; import { localize } from 'vs/nls'; import { registerColor } from 'vs/platform/theme/common/colorUtils'; -export const terminalStickyScrollBackground = registerColor('terminalStickyScroll.background', { - light: null, - dark: null, - hcDark: null, - hcLight: null -}, localize('terminalStickyScroll.background', 'The background color of the sticky scroll overlay in the terminal.')); +export const terminalStickyScrollBackground = registerColor('terminalStickyScroll.background', null, localize('terminalStickyScroll.background', 'The background color of the sticky scroll overlay in the terminal.')); export const terminalStickyScrollHoverBackground = registerColor('terminalStickyScrollHover.background', { dark: '#2A2D2E', light: '#F0F0F0', - hcDark: null, - hcLight: Color.fromHex('#0F4A85').transparent(0.1) + hcDark: '#E48B39', + hcLight: '#0f4a85' }, localize('terminalStickyScrollHover.background', 'The background color of the sticky scroll overlay in the terminal when hovered.')); + +registerColor('terminalStickyScroll.border', { + dark: null, + light: null, + hcDark: '#6fc3df', + hcLight: '#0f4a85' +}, localize('terminalStickyScroll.border', 'The border of the sticky scroll overlay in the terminal.')); diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index 194ee23694e..639ec2c8833 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type { SerializeAddon as SerializeAddonType } from '@xterm/addon-serialize'; +import type { WebglAddon as WebglAddonType } from '@xterm/addon-webgl'; import type { IBufferLine, IMarker, ITerminalOptions, ITheme, Terminal as RawXtermTerminal, Terminal as XTermTerminal } from '@xterm/xterm'; import { importAMDNodeModule } from 'vs/amdX'; import { $, addDisposableListener, addStandardDisposableListener, getWindow } from 'vs/base/browser/dom'; @@ -21,7 +22,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandDetectionCapability, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ITerminalInstance, IXtermColorProvider, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalConfigurationService, ITerminalInstance, IXtermColorProvider, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { openContextMenu } from 'vs/workbench/contrib/terminal/browser/terminalContextMenu'; import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { TERMINAL_CONFIG_SECTION, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -46,6 +47,7 @@ const enum Constants { export class TerminalStickyScrollOverlay extends Disposable { private _stickyScrollOverlay?: RawXtermTerminal; private _serializeAddon?: SerializeAddonType; + private _webglAddon?: WebglAddonType; private _element?: HTMLElement; private _currentStickyCommand?: ITerminalCommand | ICurrentPartialCommand; @@ -69,6 +71,7 @@ export class TerminalStickyScrollOverlay extends Disposable { @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IMenuService menuService: IMenuService, + @ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService, @IThemeService private readonly _themeService: IThemeService, ) { super(); @@ -101,6 +104,7 @@ export class TerminalStickyScrollOverlay extends Disposable { allowProposedApi: true, ...this._getOptions() })); + this._refreshGpuAcceleration(); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(TERMINAL_CONFIG_SECTION)) { this._syncOptions(); @@ -413,6 +417,7 @@ export class TerminalStickyScrollOverlay extends Disposable { } this._stickyScrollOverlay.resize(this._xterm.raw.cols, this._stickyScrollOverlay.rows); this._stickyScrollOverlay.options = this._getOptions(); + this._refreshGpuAcceleration(); } private _getOptions(): ITerminalOptions { @@ -435,9 +440,29 @@ export class TerminalStickyScrollOverlay extends Disposable { minimumContrastRatio: o.minimumContrastRatio, tabStopWidth: o.tabStopWidth, overviewRulerWidth: o.overviewRulerWidth, + customGlyphs: o.customGlyphs, }; } + @throttle(0) + private async _refreshGpuAcceleration() { + if (this._shouldLoadWebgl() && !this._webglAddon) { + const WebglAddon = await this._getWebglAddonConstructor(); + if (this._store.isDisposed) { + return; + } + this._webglAddon = this._register(new WebglAddon()); + this._stickyScrollOverlay?.loadAddon(this._webglAddon); + } else if (!this._shouldLoadWebgl() && this._webglAddon) { + this._webglAddon.dispose(); + this._webglAddon = undefined; + } + } + + private _shouldLoadWebgl(): boolean { + return this._terminalConfigurationService.config.gpuAcceleration === 'auto' || this._terminalConfigurationService.config.gpuAcceleration === 'on'; + } + private _getTheme(isHovering: boolean): ITheme { const theme = this._themeService.getColorTheme(); return { @@ -452,8 +477,12 @@ export class TerminalStickyScrollOverlay extends Disposable { @memoize private async _getSerializeAddonConstructor(): Promise { - const m = await importAMDNodeModule('@xterm/addon-serialize', 'lib/addon-serialize.js'); - return m.SerializeAddon; + return (await importAMDNodeModule('@xterm/addon-serialize', 'lib/addon-serialize.js')).SerializeAddon; + } + + @memoize + private async _getWebglAddonConstructor(): Promise { + return (await importAMDNodeModule('@xterm/addon-webgl', 'lib/addon-webgl.js')).WebglAddon; } } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index d46c7f3ae04..cd52628b6dc 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -16,7 +16,6 @@ import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { ITerminalConfigurationService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; import type { ITerminalAddon, Terminal } from '@xterm/xterm'; @@ -114,7 +113,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey, @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService ) { super(); @@ -159,7 +157,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest private _sync(promptInputState: IPromptInputModelState): void { const config = this._configurationService.getValue(terminalSuggestConfigSection); - const enabled = config.enabled || this._terminalConfigurationService.config.shellIntegration?.suggestEnabled; + const enabled = config.enabled; if (!enabled) { return; } @@ -222,11 +220,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } // TODO: What do frozen and auto do? const xtermBox = this._screen!.getBoundingClientRect(); - const panelBox = this._panel!.offsetParent!.getBoundingClientRect(); this._suggestWidget.showSuggestions(0, false, false, { - left: (xtermBox.left - panelBox.left) + this._terminal.buffer.active.cursorX * dimensions.width, - top: (xtermBox.top - panelBox.top) + this._terminal.buffer.active.cursorY * dimensions.height, + left: xtermBox.left + this._terminal.buffer.active.cursorX * dimensions.width, + top: xtermBox.top + this._terminal.buffer.active.cursorY * dimensions.height, height: dimensions.height }); } @@ -269,6 +266,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (!Array.isArray(completionList)) { completionList = [completionList]; } + const completions = completionList.map((e: any) => { return new SimpleCompletionItem({ label: e.ListItemText, @@ -285,7 +283,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (this._leadingLineContent.trim().includes(' ') || firstChar === '[') { replacementIndex = parseInt(args[0]); replacementLength = parseInt(args[1]); - this._leadingLineContent = completions[0]?.completion.label.slice(0, replacementLength) ?? ''; + const firstCompletion = completions[0]?.completion; + this._leadingLineContent = (firstCompletion?.completionText ?? firstCompletion?.label)?.slice(0, replacementLength) ?? ''; } else { completions.push(...this._cachedPwshCommands); } @@ -407,7 +406,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._leadingLineContent = completions[0].completion.label.slice(0, replacementLength); const model = new SimpleCompletionModel(completions, new LineContext(this._leadingLineContent, replacementIndex), replacementIndex, replacementLength); if (completions.length === 1) { - const insertText = completions[0].completion.label.substring(replacementLength); + const insertText = (completions[0].completion.completionText ?? completions[0].completion.label).substring(replacementLength); if (insertText.length === 0) { this._onBell.fire(); return; @@ -435,7 +434,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } // TODO: What do frozen and auto do? const xtermBox = this._screen!.getBoundingClientRect(); - const panelBox = this._panel!.offsetParent!.getBoundingClientRect(); this._initialPromptInputState = { value: this._promptInputModel.value, cursorIndex: this._promptInputModel.cursorIndex, @@ -443,8 +441,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest }; suggestWidget.setCompletionModel(model); suggestWidget.showSuggestions(0, false, false, { - left: (xtermBox.left - panelBox.left) + this._terminal.buffer.active.cursorX * dimensions.width, - top: (xtermBox.top - panelBox.top) + this._terminal.buffer.active.cursorY * dimensions.height, + left: xtermBox.left + this._terminal.buffer.active.cursorX * dimensions.width, + top: xtermBox.top + this._terminal.buffer.active.cursorY * dimensions.height, height: dimensions.height }); } @@ -505,6 +503,12 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest const completionText = completion.completionText ?? completion.label; const finalCompletionRightSide = completionText.substring((this._leadingLineContent?.length ?? 0) - (lastSpaceIndex === -1 ? 0 : lastSpaceIndex + 1)); + // Hide the widget if there is no change + if (finalCompletionRightSide === additionalInput) { + this.hideSuggestWidget(); + return; + } + // Get the final completion on the right side of the cursor if it differs from the initial // propmt input state let finalCompletionLeftSide = completionText.substring(0, (this._leadingLineContent?.length ?? 0) - (lastSpaceIndex === -1 ? 0 : lastSpaceIndex + 1)); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index f465e37f076..357a6bbee60 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -10,7 +10,6 @@ import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; export const enum TerminalSuggestSettingId { Enabled = 'terminal.integrated.suggest.enabled', - EnabledLegacy = 'terminal.integrated.shellIntegration.suggestEnabled', QuickSuggestions = 'terminal.integrated.suggest.quickSuggestions', SuggestOnTriggerCharacters = 'terminal.integrated.suggest.suggestOnTriggerCharacters', } @@ -30,13 +29,6 @@ export const terminalSuggestConfiguration: IStringDictionary\",\"ResultType\":11,\"ToolTip\":\"System.Collections.ObjectModel.ReadOnlyCollection[T]\"},{\"CompletionText\":\"System.Collections.ReadOnlyCollectionBase\",\"ListItemText\":\"ReadOnlyCollectionBase\",\"ResultType\":11,\"ToolTip\":\"System.Collections.ReadOnlyCollectionBase\"},{\"CompletionText\":\"System.Runtime.CompilerServices.ReadOnlyCollectionBuilder\",\"ListItemText\":\"ReadOnlyCollectionBuilder<>\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.ReadOnlyCollectionBuilder[T]\"},{\"CompletionText\":\"System.Collections.ObjectModel.ReadOnlyDictionary\",\"ListItemText\":\"ReadOnlyDictionary<>\",\"ResultType\":11,\"ToolTip\":\"System.Collections.ObjectModel.ReadOnlyDictionary[T1, T2]\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReadOnlyDirectoryServerCollection\",\"ListItemText\":\"ReadOnlyDirectoryServerCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReadOnlyDirectoryServerCollection\"},{\"CompletionText\":\"System.Data.ReadOnlyException\",\"ListItemText\":\"ReadOnlyException\",\"ResultType\":11,\"ToolTip\":\"System.Data.ReadOnlyException\"},{\"CompletionText\":\"Json.Schema.ReadOnlyKeyword\",\"ListItemText\":\"ReadOnlyKeyword\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.ReadOnlyKeyword\"},{\"CompletionText\":\"System.ReadOnlyMemory\",\"ListItemText\":\"ReadOnlyMemory<>\",\"ResultType\":11,\"ToolTip\":\"System.ReadOnlyMemory[T]\"},{\"CompletionText\":\"System.Net.Http.ReadOnlyMemoryContent\",\"ListItemText\":\"ReadOnlyMemoryContent\",\"ResultType\":11,\"ToolTip\":\"System.Net.Http.ReadOnlyMemoryContent\"},{\"CompletionText\":\"System.Collections.ObjectModel.ReadOnlyObservableCollection\",\"ListItemText\":\"ReadOnlyObservableCollection<>\",\"ResultType\":11,\"ToolTip\":\"System.Collections.ObjectModel.ReadOnlyObservableCollection[T]\"},{\"CompletionText\":\"System.Management.Automation.ReadOnlyPSMemberInfoCollection\",\"ListItemText\":\"ReadOnlyPSMemberInfoCollection<>\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.ReadOnlyPSMemberInfoCollection[T]\"},{\"CompletionText\":\"System.Buffers.ReadOnlySequence\",\"ListItemText\":\"ReadOnlySequence<>\",\"ResultType\":11,\"ToolTip\":\"System.Buffers.ReadOnlySequence[T]\"},{\"CompletionText\":\"System.Buffers.ReadOnlySequenceSegment\",\"ListItemText\":\"ReadOnlySequenceSegment<>\",\"ResultType\":11,\"ToolTip\":\"System.Buffers.ReadOnlySequenceSegment[T]\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteCollection\",\"ListItemText\":\"ReadOnlySiteCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteLinkBridgeCollection\",\"ListItemText\":\"ReadOnlySiteLinkBridgeCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteLinkBridgeCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteLinkCollection\",\"ListItemText\":\"ReadOnlySiteLinkCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReadOnlySiteLinkCollection\"},{\"CompletionText\":\"System.ReadOnlySpan\",\"ListItemText\":\"ReadOnlySpan<>\",\"ResultType\":11,\"ToolTip\":\"System.ReadOnlySpan[T]\"},{\"CompletionText\":\"System.Buffers.ReadOnlySpanAction\",\"ListItemText\":\"ReadOnlySpanAction<>\",\"ResultType\":11,\"ToolTip\":\"System.Buffers.ReadOnlySpanAction[T1, T2]\"},{\"CompletionText\":\"System.Runtime.InteropServices.Marshalling.ReadOnlySpanMarshaller\",\"ListItemText\":\"ReadOnlySpanMarshaller<>\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.InteropServices.Marshalling.ReadOnlySpanMarshaller[T1, T2]\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReadOnlyStringCollection\",\"ListItemText\":\"ReadOnlyStringCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReadOnlyStringCollection\"},{\"CompletionText\":\"System.Xml.ReadState\",\"ListItemText\":\"ReadState\",\"ResultType\":11,\"ToolTip\":\"System.Xml.ReadState\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.ReceiveJobCommand\",\"ListItemText\":\"ReceiveJobCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.ReceiveJobCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.ReceivePSSessionCommand\",\"ListItemText\":\"ReceivePSSessionCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.ReceivePSSessionCommand\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.RecipientInfo\",\"ListItemText\":\"RecipientInfo\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.RecipientInfo\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.RecipientInfoCollection\",\"ListItemText\":\"RecipientInfoCollection\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.RecipientInfoCollection\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.RecipientInfoEnumerator\",\"ListItemText\":\"RecipientInfoEnumerator\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.RecipientInfoEnumerator\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.RecipientInfoType\",\"ListItemText\":\"RecipientInfoType\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.RecipientInfoType\"},{\"CompletionText\":\"System.Speech.Recognition.RecognitionEventArgs\",\"ListItemText\":\"RecognitionEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognitionEventArgs\"},{\"CompletionText\":\"System.Speech.Recognition.RecognitionResult\",\"ListItemText\":\"RecognitionResult\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognitionResult\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizeCompletedEventArgs\",\"ListItemText\":\"RecognizeCompletedEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizeCompletedEventArgs\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizedAudio\",\"ListItemText\":\"RecognizedAudio\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizedAudio\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizedPhrase\",\"ListItemText\":\"RecognizedPhrase\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizedPhrase\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizedWordUnit\",\"ListItemText\":\"RecognizedWordUnit\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizedWordUnit\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizeMode\",\"ListItemText\":\"RecognizeMode\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizeMode\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizerInfo\",\"ListItemText\":\"RecognizerInfo\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizerInfo\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizerState\",\"ListItemText\":\"RecognizerState\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizerState\"},{\"CompletionText\":\"System.Speech.Recognition.RecognizerUpdateReachedEventArgs\",\"ListItemText\":\"RecognizerUpdateReachedEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.RecognizerUpdateReachedEventArgs\"},{\"CompletionText\":\"System.ComponentModel.RecommendedAsConfigurableAttribute\",\"ListItemText\":\"RecommendedAsConfigurableAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RecommendedAsConfigurableAttribute\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RecordDeclarationSyntax\",\"ListItemText\":\"RecordDeclarationSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RecordDeclarationSyntax\"},{\"CompletionText\":\".Interop+Gdi32+RECT\",\"ListItemText\":\"RECT\",\"ResultType\":11,\"ToolTip\":\".Interop+Gdi32+RECT\"},{\"CompletionText\":\"System.Drawing.Rectangle\",\"ListItemText\":\"Rectangle\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.Rectangle\"},{\"CompletionText\":\"System.Management.Automation.Host.Rectangle\",\"ListItemText\":\"Rectangle\",\"ResultType\":11,\"ToolTip\":\"Struct System.Management.Automation.Host.Rectangle\"},{\"CompletionText\":\"System.Drawing.RectangleConverter\",\"ListItemText\":\"RectangleConverter\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.RectangleConverter\"},{\"CompletionText\":\"System.Drawing.RectangleF\",\"ListItemText\":\"RectangleF\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.RectangleF\"},{\"CompletionText\":\"Json.Schema.RecursiveAnchorKeyword\",\"ListItemText\":\"RecursiveAnchorKeyword\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.RecursiveAnchorKeyword\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RecursivePatternSyntax\",\"ListItemText\":\"RecursivePatternSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RecursivePatternSyntax\"},{\"CompletionText\":\"Json.Schema.RecursiveRefKeyword\",\"ListItemText\":\"RecursiveRefKeyword\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.RecursiveRefKeyword\"},{\"CompletionText\":\"Microsoft.VisualBasic.FileIO.RecycleOption\",\"ListItemText\":\"RecycleOption\",\"ResultType\":11,\"ToolTip\":\"Microsoft.VisualBasic.FileIO.RecycleOption\"},{\"CompletionText\":\"System.Management.Automation.RedirectedException\",\"ListItemText\":\"RedirectedException\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RedirectedException\"},{\"CompletionText\":\"System.Management.Automation.Language.RedirectionAst\",\"ListItemText\":\"RedirectionAst\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Language.RedirectionAst\"},{\"CompletionText\":\"System.Management.Automation.Language.RedirectionStream\",\"ListItemText\":\"RedirectionStream\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Language.RedirectionStream\"},{\"CompletionText\":\"System.Management.Automation.Language.RedirectionToken\",\"ListItemText\":\"RedirectionToken\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Language.RedirectionToken\"},{\"CompletionText\":\"System.Security.Cryptography.Xml.Reference\",\"ListItemText\":\"Reference\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Xml.Reference\"},{\"CompletionText\":\"System.Runtime.CompilerServices.ReferenceAssemblyAttribute\",\"ListItemText\":\"ReferenceAssemblyAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.ReferenceAssemblyAttribute\"},{\"CompletionText\":\"System.ComponentModel.ReferenceConverter\",\"ListItemText\":\"ReferenceConverter\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.ReferenceConverter\"},{\"CompletionText\":\"System.ServiceModel.Syndication.ReferencedCategoriesDocument\",\"ListItemText\":\"ReferencedCategoriesDocument\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Syndication.ReferencedCategoriesDocument\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.ReferenceDirectiveTriviaSyntax\",\"ListItemText\":\"ReferenceDirectiveTriviaSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.ReferenceDirectiveTriviaSyntax\"},{\"CompletionText\":\"System.Collections.Generic.ReferenceEqualityComparer\",\"ListItemText\":\"ReferenceEqualityComparer\",\"ResultType\":11,\"ToolTip\":\"System.Collections.Generic.ReferenceEqualityComparer\"},{\"CompletionText\":\"System.Text.Json.Serialization.ReferenceHandler\",\"ListItemText\":\"ReferenceHandler\",\"ResultType\":11,\"ToolTip\":\"System.Text.Json.Serialization.ReferenceHandler\"},{\"CompletionText\":\"System.Security.Cryptography.Xml.ReferenceList\",\"ListItemText\":\"ReferenceList\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Xml.ReferenceList\"},{\"CompletionText\":\"Newtonsoft.Json.ReferenceLoopHandling\",\"ListItemText\":\"ReferenceLoopHandling\",\"ResultType\":11,\"ToolTip\":\"Newtonsoft.Json.ReferenceLoopHandling\"},{\"CompletionText\":\"System.Text.Json.Serialization.ReferenceResolver\",\"ListItemText\":\"ReferenceResolver\",\"ResultType\":11,\"ToolTip\":\"System.Text.Json.Serialization.ReferenceResolver\"},{\"CompletionText\":\"System.DirectoryServices.Protocols.ReferralCallback\",\"ListItemText\":\"ReferralCallback\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.Protocols.ReferralCallback\"},{\"CompletionText\":\"System.DirectoryServices.ReferralChasingOption\",\"ListItemText\":\"ReferralChasingOption\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ReferralChasingOption\"},{\"CompletionText\":\"System.DirectoryServices.Protocols.ReferralChasingOptions\",\"ListItemText\":\"ReferralChasingOptions\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.Protocols.ReferralChasingOptions\"},{\"CompletionText\":\"Markdig.Extensions.ReferralLinks.ReferralLinksExtension\",\"ListItemText\":\"ReferralLinksExtension\",\"ResultType\":11,\"ToolTip\":\"Markdig.Extensions.ReferralLinks.ReferralLinksExtension\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefExpressionSyntax\",\"ListItemText\":\"RefExpressionSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefExpressionSyntax\"},{\"CompletionText\":\"Json.Schema.RefKeyword\",\"ListItemText\":\"RefKeyword\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.RefKeyword\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.RefKind\",\"ListItemText\":\"RefKind\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.RefKind\"},{\"CompletionText\":\"Newtonsoft.Json.Serialization.ReflectionAttributeProvider\",\"ListItemText\":\"ReflectionAttributeProvider\",\"ResultType\":11,\"ToolTip\":\"Newtonsoft.Json.Serialization.ReflectionAttributeProvider\"},{\"CompletionText\":\"System.Reflection.ReflectionContext\",\"ListItemText\":\"ReflectionContext\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.ReflectionContext\"},{\"CompletionText\":\"System.ComponentModel.Composition.ReflectionModel.ReflectionModelServices\",\"ListItemText\":\"ReflectionModelServices\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.Composition.ReflectionModel.ReflectionModelServices\"},{\"CompletionText\":\"System.Security.Permissions.ReflectionPermission\",\"ListItemText\":\"ReflectionPermission\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.ReflectionPermission\"},{\"CompletionText\":\"System.Security.Permissions.ReflectionPermissionAttribute\",\"ListItemText\":\"ReflectionPermissionAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.ReflectionPermissionAttribute\"},{\"CompletionText\":\"System.Security.Permissions.ReflectionPermissionFlag\",\"ListItemText\":\"ReflectionPermissionFlag\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.ReflectionPermissionFlag\"},{\"CompletionText\":\"System.Reflection.ReflectionTypeLoadException\",\"ListItemText\":\"ReflectionTypeLoadException\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.ReflectionTypeLoadException\"},{\"CompletionText\":\"System.Management.Automation.Language.ReflectionTypeName\",\"ListItemText\":\"ReflectionTypeName\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Language.ReflectionTypeName\"},{\"CompletionText\":\"Newtonsoft.Json.Serialization.ReflectionValueProvider\",\"ListItemText\":\"ReflectionValueProvider\",\"ResultType\":11,\"ToolTip\":\"Newtonsoft.Json.Serialization.ReflectionValueProvider\"},{\"CompletionText\":\"System.ComponentModel.RefreshEventArgs\",\"ListItemText\":\"RefreshEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RefreshEventArgs\"},{\"CompletionText\":\"System.ComponentModel.RefreshEventHandler\",\"ListItemText\":\"RefreshEventHandler\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RefreshEventHandler\"},{\"CompletionText\":\"System.ComponentModel.RefreshProperties\",\"ListItemText\":\"RefreshProperties\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RefreshProperties\"},{\"CompletionText\":\"System.ComponentModel.RefreshPropertiesAttribute\",\"ListItemText\":\"RefreshPropertiesAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RefreshPropertiesAttribute\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RefSafetyRulesAttribute\",\"ListItemText\":\"RefSafetyRulesAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RefSafetyRulesAttribute\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefTypeExpressionSyntax\",\"ListItemText\":\"RefTypeExpressionSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefTypeExpressionSyntax\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefTypeSyntax\",\"ListItemText\":\"RefTypeSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefTypeSyntax\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefValueExpressionSyntax\",\"ListItemText\":\"RefValueExpressionSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RefValueExpressionSyntax\"},{\"CompletionText\":\"regex\",\"ListItemText\":\"Regex\",\"ResultType\":11,\"ToolTip\":\"Class System.Text.RegularExpressions.Regex\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexCompilationInfo\",\"ListItemText\":\"RegexCompilationInfo\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexCompilationInfo\"},{\"CompletionText\":\"Newtonsoft.Json.Converters.RegexConverter\",\"ListItemText\":\"RegexConverter\",\"ResultType\":11,\"ToolTip\":\"Newtonsoft.Json.Converters.RegexConverter\"},{\"CompletionText\":\"Json.Schema.RegexFormat\",\"ListItemText\":\"RegexFormat\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.RegexFormat\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexMatchTimeoutException\",\"ListItemText\":\"RegexMatchTimeoutException\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexMatchTimeoutException\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexOptions\",\"ListItemText\":\"RegexOptions\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexOptions\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexParseError\",\"ListItemText\":\"RegexParseError\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexParseError\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexParseException\",\"ListItemText\":\"RegexParseException\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexParseException\"},{\"CompletionText\":\"JetBrains.Annotations.RegexPatternAttribute\",\"ListItemText\":\"RegexPatternAttribute\",\"ResultType\":11,\"ToolTip\":\"JetBrains.Annotations.RegexPatternAttribute\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexRunner\",\"ListItemText\":\"RegexRunner\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexRunner\"},{\"CompletionText\":\"System.Text.RegularExpressions.RegexRunnerFactory\",\"ListItemText\":\"RegexRunnerFactory\",\"ResultType\":11,\"ToolTip\":\"System.Text.RegularExpressions.RegexRunnerFactory\"},{\"CompletionText\":\"System.Configuration.RegexStringValidator\",\"ListItemText\":\"RegexStringValidator\",\"ResultType\":11,\"ToolTip\":\"System.Configuration.RegexStringValidator\"},{\"CompletionText\":\"System.Configuration.RegexStringValidatorAttribute\",\"ListItemText\":\"RegexStringValidatorAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Configuration.RegexStringValidatorAttribute\"},{\"CompletionText\":\"System.Drawing.Region\",\"ListItemText\":\"Region\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.Region\"},{\"CompletionText\":\"System.Drawing.Drawing2D.RegionData\",\"ListItemText\":\"RegionData\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.Drawing2D.RegionData\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RegionDirectiveTriviaSyntax\",\"ListItemText\":\"RegionDirectiveTriviaSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RegionDirectiveTriviaSyntax\"},{\"CompletionText\":\"System.Globalization.RegionInfo\",\"ListItemText\":\"RegionInfo\",\"ResultType\":11,\"ToolTip\":\"System.Globalization.RegionInfo\"},{\"CompletionText\":\"System.Management.Automation.RegisterArgumentCompleterCommand\",\"ListItemText\":\"RegisterArgumentCompleterCommand\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RegisterArgumentCompleterCommand\"},{\"CompletionText\":\"System.Threading.RegisteredWaitHandle\",\"ListItemText\":\"RegisteredWaitHandle\",\"ResultType\":11,\"ToolTip\":\"System.Threading.RegisteredWaitHandle\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RegisterEngineEventCommand\",\"ListItemText\":\"RegisterEngineEventCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RegisterEngineEventCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RegisterObjectEventCommand\",\"ListItemText\":\"RegisterObjectEventCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RegisterObjectEventCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RegisterPSSessionConfigurationCommand\",\"ListItemText\":\"RegisterPSSessionConfigurationCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RegisterPSSessionConfigurationCommand\"},{\"CompletionText\":\"System.ComponentModel.Composition.Registration.RegistrationBuilder\",\"ListItemText\":\"RegistrationBuilder\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.Composition.Registration.RegistrationBuilder\"},{\"CompletionText\":\"Microsoft.Win32.Registry\",\"ListItemText\":\"Registry\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.Registry\"},{\"CompletionText\":\"System.Security.AccessControl.RegistryAccessRule\",\"ListItemText\":\"RegistryAccessRule\",\"ResultType\":11,\"ToolTip\":\"System.Security.AccessControl.RegistryAccessRule\"},{\"CompletionText\":\"Microsoft.Win32.RegistryAclExtensions\",\"ListItemText\":\"RegistryAclExtensions\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryAclExtensions\"},{\"CompletionText\":\"System.Security.AccessControl.RegistryAuditRule\",\"ListItemText\":\"RegistryAuditRule\",\"ResultType\":11,\"ToolTip\":\"System.Security.AccessControl.RegistryAuditRule\"},{\"CompletionText\":\"Microsoft.Win32.RegistryHive\",\"ListItemText\":\"RegistryHive\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryHive\"},{\"CompletionText\":\"Microsoft.Win32.RegistryKey\",\"ListItemText\":\"RegistryKey\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryKey\"},{\"CompletionText\":\"Microsoft.Win32.RegistryKeyPermissionCheck\",\"ListItemText\":\"RegistryKeyPermissionCheck\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryKeyPermissionCheck\"},{\"CompletionText\":\"Microsoft.Win32.RegistryOptions\",\"ListItemText\":\"RegistryOptions\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryOptions\"},{\"CompletionText\":\"System.Security.Permissions.RegistryPermission\",\"ListItemText\":\"RegistryPermission\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.RegistryPermission\"},{\"CompletionText\":\"System.Security.Permissions.RegistryPermissionAccess\",\"ListItemText\":\"RegistryPermissionAccess\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.RegistryPermissionAccess\"},{\"CompletionText\":\"System.Security.Permissions.RegistryPermissionAttribute\",\"ListItemText\":\"RegistryPermissionAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.RegistryPermissionAttribute\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RegistryProvider\",\"ListItemText\":\"RegistryProvider\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RegistryProvider\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RegistryProviderSetItemDynamicParameter\",\"ListItemText\":\"RegistryProviderSetItemDynamicParameter\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RegistryProviderSetItemDynamicParameter\"},{\"CompletionText\":\"System.Security.AccessControl.RegistryRights\",\"ListItemText\":\"RegistryRights\",\"ResultType\":11,\"ToolTip\":\"System.Security.AccessControl.RegistryRights\"},{\"CompletionText\":\"System.Security.AccessControl.RegistrySecurity\",\"ListItemText\":\"RegistrySecurity\",\"ResultType\":11,\"ToolTip\":\"System.Security.AccessControl.RegistrySecurity\"},{\"CompletionText\":\"Microsoft.Win32.RegistryValueKind\",\"ListItemText\":\"RegistryValueKind\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryValueKind\"},{\"CompletionText\":\"Microsoft.Win32.RegistryValueOptions\",\"ListItemText\":\"RegistryValueOptions\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryValueOptions\"},{\"CompletionText\":\"Microsoft.Win32.RegistryView\",\"ListItemText\":\"RegistryView\",\"ResultType\":11,\"ToolTip\":\"Microsoft.Win32.RegistryView\"},{\"CompletionText\":\"System.ComponentModel.DataAnnotations.RegularExpressionAttribute\",\"ListItemText\":\"RegularExpressionAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.DataAnnotations.RegularExpressionAttribute\"},{\"CompletionText\":\"System.Management.RelatedObjectQuery\",\"ListItemText\":\"RelatedObjectQuery\",\"ResultType\":11,\"ToolTip\":\"System.Management.RelatedObjectQuery\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RelationalPatternSyntax\",\"ListItemText\":\"RelationalPatternSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.RelationalPatternSyntax\"},{\"CompletionText\":\"System.Management.RelationshipQuery\",\"ListItemText\":\"RelationshipQuery\",\"ResultType\":11,\"ToolTip\":\"System.Management.RelationshipQuery\"},{\"CompletionText\":\"Json.Pointer.RelativeJsonPointer\",\"ListItemText\":\"RelativeJsonPointer\",\"ResultType\":11,\"ToolTip\":\"Json.Pointer.RelativeJsonPointer\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.PooledObjects.PooledDelegates+Releaser\",\"ListItemText\":\"Releaser\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.PooledObjects.PooledDelegates+Releaser\"},{\"CompletionText\":\"System.Runtime.ConstrainedExecution.ReliabilityContractAttribute\",\"ListItemText\":\"ReliabilityContractAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.ConstrainedExecution.ReliabilityContractAttribute\"},{\"CompletionText\":\"System.ServiceModel.ReliableMessagingVersion\",\"ListItemText\":\"ReliableMessagingVersion\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.ReliableMessagingVersion\"},{\"CompletionText\":\"System.ServiceModel.ReliableSession\",\"ListItemText\":\"ReliableSession\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.ReliableSession\"},{\"CompletionText\":\"System.ServiceModel.Channels.ReliableSessionBindingElement\",\"ListItemText\":\"ReliableSessionBindingElement\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Channels.ReliableSessionBindingElement\"},{\"CompletionText\":\"System.Net.Security.RemoteCertificateValidationCallback\",\"ListItemText\":\"RemoteCertificateValidationCallback\",\"ResultType\":11,\"ToolTip\":\"System.Net.Security.RemoteCertificateValidationCallback\"},{\"CompletionText\":\"System.Management.Automation.RemoteCommandInfo\",\"ListItemText\":\"RemoteCommandInfo\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RemoteCommandInfo\"},{\"CompletionText\":\"System.Management.Automation.RemoteException\",\"ListItemText\":\"RemoteException\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RemoteException\"},{\"CompletionText\":\"System.Management.Automation.Remoting.RemoteSessionNamedPipeServer\",\"ListItemText\":\"RemoteSessionNamedPipeServer\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Remoting.RemoteSessionNamedPipeServer\"},{\"CompletionText\":\"System.Management.Automation.RemoteStreamOptions\",\"ListItemText\":\"RemoteStreamOptions\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.RemoteStreamOptions\"},{\"CompletionText\":\"System.Management.Automation.RemotingBehavior\",\"ListItemText\":\"RemotingBehavior\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.RemotingBehavior\"},{\"CompletionText\":\"System.Management.Automation.RemotingCapability\",\"ListItemText\":\"RemotingCapability\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.RemotingCapability\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingDebugRecord\",\"ListItemText\":\"RemotingDebugRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingDebugRecord\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingErrorRecord\",\"ListItemText\":\"RemotingErrorRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingErrorRecord\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.Internal.RemotingErrorResources\",\"ListItemText\":\"RemotingErrorResources\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.Internal.RemotingErrorResources\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingInformationRecord\",\"ListItemText\":\"RemotingInformationRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingInformationRecord\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingProgressRecord\",\"ListItemText\":\"RemotingProgressRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingProgressRecord\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingVerboseRecord\",\"ListItemText\":\"RemotingVerboseRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingVerboseRecord\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RemotingWarningRecord\",\"ListItemText\":\"RemotingWarningRecord\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RemotingWarningRecord\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveAliasCommand\",\"ListItemText\":\"RemoveAliasCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveAliasCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveEventCommand\",\"ListItemText\":\"RemoveEventCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveEventCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveItemCommand\",\"ListItemText\":\"RemoveItemCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveItemCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveItemPropertyCommand\",\"ListItemText\":\"RemoveItemPropertyCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveItemPropertyCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveJobCommand\",\"ListItemText\":\"RemoveJobCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveJobCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.RemoveKeyHandlerCommand\",\"ListItemText\":\"RemoveKeyHandlerCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.RemoveKeyHandlerCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveModuleCommand\",\"ListItemText\":\"RemoveModuleCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveModuleCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemovePSBreakpointCommand\",\"ListItemText\":\"RemovePSBreakpointCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemovePSBreakpointCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemovePSDriveCommand\",\"ListItemText\":\"RemovePSDriveCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemovePSDriveCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemovePSSessionCommand\",\"ListItemText\":\"RemovePSSessionCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemovePSSessionCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveServiceCommand\",\"ListItemText\":\"RemoveServiceCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveServiceCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveTypeDataCommand\",\"ListItemText\":\"RemoveTypeDataCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveTypeDataCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RemoveVariableCommand\",\"ListItemText\":\"RemoveVariableCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RemoveVariableCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RenameComputerChangeInfo\",\"ListItemText\":\"RenameComputerChangeInfo\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RenameComputerChangeInfo\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RenameComputerCommand\",\"ListItemText\":\"RenameComputerCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RenameComputerCommand\"},{\"CompletionText\":\"System.IO.RenamedEventArgs\",\"ListItemText\":\"RenamedEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.IO.RenamedEventArgs\"},{\"CompletionText\":\"System.IO.RenamedEventHandler\",\"ListItemText\":\"RenamedEventHandler\",\"ResultType\":11,\"ToolTip\":\"System.IO.RenamedEventHandler\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RenameItemCommand\",\"ListItemText\":\"RenameItemCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RenameItemCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RenameItemPropertyCommand\",\"ListItemText\":\"RenameItemPropertyCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RenameItemPropertyCommand\"},{\"CompletionText\":\"Markdig.Renderers.RendererBase\",\"ListItemText\":\"RendererBase\",\"ResultType\":11,\"ToolTip\":\"Markdig.Renderers.RendererBase\"},{\"CompletionText\":\"System.Speech.Recognition.ReplacementText\",\"ListItemText\":\"ReplacementText\",\"ResultType\":11,\"ToolTip\":\"System.Speech.Recognition.ReplacementText\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationConnection\",\"ListItemText\":\"ReplicationConnection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationConnection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationConnectionCollection\",\"ListItemText\":\"ReplicationConnectionCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationConnectionCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationCursor\",\"ListItemText\":\"ReplicationCursor\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationCursor\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationCursorCollection\",\"ListItemText\":\"ReplicationCursorCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationCursorCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationFailure\",\"ListItemText\":\"ReplicationFailure\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationFailure\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationFailureCollection\",\"ListItemText\":\"ReplicationFailureCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationFailureCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighbor\",\"ListItemText\":\"ReplicationNeighbor\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighbor\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighborCollection\",\"ListItemText\":\"ReplicationNeighborCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighborCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighbor+ReplicationNeighborOptions\",\"ListItemText\":\"ReplicationNeighborOptions\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationNeighbor+ReplicationNeighborOptions\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperation\",\"ListItemText\":\"ReplicationOperation\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperation\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationCollection\",\"ListItemText\":\"ReplicationOperationCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationCollection\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationInformation\",\"ListItemText\":\"ReplicationOperationInformation\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationInformation\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationType\",\"ListItemText\":\"ReplicationOperationType\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationOperationType\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationSecurityLevel\",\"ListItemText\":\"ReplicationSecurityLevel\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationSecurityLevel\"},{\"CompletionText\":\"System.DirectoryServices.ActiveDirectory.ReplicationSpan\",\"ListItemText\":\"ReplicationSpan\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ActiveDirectory.ReplicationSpan\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.ReportDiagnostic\",\"ListItemText\":\"ReportDiagnostic\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.ReportDiagnostic\"},{\"CompletionText\":\"System.Management.Automation.Repository\",\"ListItemText\":\"Repository<>\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Repository[T]\"},{\"CompletionText\":\"Microsoft.ApplicationInsights.Extensibility.Implementation.Metrics.MetricTerms+Autocollection+Request\",\"ListItemText\":\"Request\",\"ResultType\":11,\"ToolTip\":\"Microsoft.ApplicationInsights.Extensibility.Implementation.Metrics.MetricTerms+Autocollection+Request\"},{\"CompletionText\":\"System.Net.Cache.RequestCacheLevel\",\"ListItemText\":\"RequestCacheLevel\",\"ResultType\":11,\"ToolTip\":\"System.Net.Cache.RequestCacheLevel\"},{\"CompletionText\":\"System.Net.Cache.RequestCachePolicy\",\"ListItemText\":\"RequestCachePolicy\",\"ResultType\":11,\"ToolTip\":\"System.Net.Cache.RequestCachePolicy\"},{\"CompletionText\":\"System.ServiceModel.Channels.RequestContext\",\"ListItemText\":\"RequestContext\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Channels.RequestContext\"},{\"CompletionText\":\"Microsoft.ApplicationInsights.Extensibility.Implementation.Metrics.MetricTerms+Autocollection+Metric+RequestDuration\",\"ListItemText\":\"RequestDuration\",\"ResultType\":11,\"ToolTip\":\"Microsoft.ApplicationInsights.Extensibility.Implementation.Metrics.MetricTerms+Autocollection+Metric+RequestDuration\"},{\"CompletionText\":\"Microsoft.ApplicationInsights.DataContracts.RequestTelemetry\",\"ListItemText\":\"RequestTelemetry\",\"ResultType\":11,\"ToolTip\":\"Microsoft.ApplicationInsights.DataContracts.RequestTelemetry\"},{\"CompletionText\":\"Newtonsoft.Json.Required\",\"ListItemText\":\"Required\",\"ResultType\":11,\"ToolTip\":\"Newtonsoft.Json.Required\"},{\"CompletionText\":\"System.ComponentModel.DataAnnotations.RequiredAttribute\",\"ListItemText\":\"RequiredAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.DataAnnotations.RequiredAttribute\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RequiredAttributeAttribute\",\"ListItemText\":\"RequiredAttributeAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RequiredAttributeAttribute\"},{\"CompletionText\":\"Json.Schema.RequiredKeyword\",\"ListItemText\":\"RequiredKeyword\",\"ResultType\":11,\"ToolTip\":\"Json.Schema.RequiredKeyword\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RequiredMemberAttribute\",\"ListItemText\":\"RequiredMemberAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RequiredMemberAttribute\"},{\"CompletionText\":\"System.Diagnostics.CodeAnalysis.RequiresAssemblyFilesAttribute\",\"ListItemText\":\"RequiresAssemblyFilesAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Diagnostics.CodeAnalysis.RequiresAssemblyFilesAttribute\"},{\"CompletionText\":\"System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute\",\"ListItemText\":\"RequiresDynamicCodeAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RequiresLocationAttribute\",\"ListItemText\":\"RequiresLocationAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RequiresLocationAttribute\"},{\"CompletionText\":\"System.Runtime.Versioning.RequiresPreviewFeaturesAttribute\",\"ListItemText\":\"RequiresPreviewFeaturesAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.Versioning.RequiresPreviewFeaturesAttribute\"},{\"CompletionText\":\"JetBrains.Annotations.RequireStaticDelegateAttribute\",\"ListItemText\":\"RequireStaticDelegateAttribute\",\"ResultType\":11,\"ToolTip\":\"JetBrains.Annotations.RequireStaticDelegateAttribute\"},{\"CompletionText\":\"System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute\",\"ListItemText\":\"RequiresUnreferencedCodeAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute\"},{\"CompletionText\":\"System.Reflection.Metadata.ReservedBlob\",\"ListItemText\":\"ReservedBlob<>\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.Metadata.ReservedBlob[T]\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.ResetCapability\",\"ListItemText\":\"ResetCapability\",\"ResultType\":11,\"ToolTip\":\"Enum Microsoft.PowerShell.Commands.ResetCapability\"},{\"CompletionText\":\"System.Management.Automation.ResolutionPurpose\",\"ListItemText\":\"ResolutionPurpose\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.ResolutionPurpose\"},{\"CompletionText\":\"System.ResolveEventArgs\",\"ListItemText\":\"ResolveEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.ResolveEventArgs\"},{\"CompletionText\":\"System.ResolveEventHandler\",\"ListItemText\":\"ResolveEventHandler\",\"ResultType\":11,\"ToolTip\":\"System.ResolveEventHandler\"},{\"CompletionText\":\"System.ComponentModel.Design.Serialization.ResolveNameEventArgs\",\"ListItemText\":\"ResolveNameEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.Design.Serialization.ResolveNameEventArgs\"},{\"CompletionText\":\"System.ComponentModel.Design.Serialization.ResolveNameEventHandler\",\"ListItemText\":\"ResolveNameEventHandler\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.Design.Serialization.ResolveNameEventHandler\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.ResolvePathCommand\",\"ListItemText\":\"ResolvePathCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.ResolvePathCommand\"},{\"CompletionText\":\"System.Reflection.ResourceAttributes\",\"ListItemText\":\"ResourceAttributes\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.ResourceAttributes\"},{\"CompletionText\":\"System.ServiceModel.Syndication.ResourceCollectionInfo\",\"ListItemText\":\"ResourceCollectionInfo\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Syndication.ResourceCollectionInfo\"},{\"CompletionText\":\"System.Runtime.Versioning.ResourceConsumptionAttribute\",\"ListItemText\":\"ResourceConsumptionAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.Versioning.ResourceConsumptionAttribute\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.ResourceDescription\",\"ListItemText\":\"ResourceDescription\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.ResourceDescription\"},{\"CompletionText\":\"System.Runtime.Versioning.ResourceExposureAttribute\",\"ListItemText\":\"ResourceExposureAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.Versioning.ResourceExposureAttribute\"},{\"CompletionText\":\"System.Reflection.ResourceLocation\",\"ListItemText\":\"ResourceLocation\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.ResourceLocation\"},{\"CompletionText\":\"System.Resources.ResourceManager\",\"ListItemText\":\"ResourceManager\",\"ResultType\":11,\"ToolTip\":\"System.Resources.ResourceManager\"},{\"CompletionText\":\"System.Security.Permissions.ResourcePermissionBase\",\"ListItemText\":\"ResourcePermissionBase\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.ResourcePermissionBase\"},{\"CompletionText\":\"System.Security.Permissions.ResourcePermissionBaseEntry\",\"ListItemText\":\"ResourcePermissionBaseEntry\",\"ResultType\":11,\"ToolTip\":\"System.Security.Permissions.ResourcePermissionBaseEntry\"},{\"CompletionText\":\"System.Resources.ResourceReader\",\"ListItemText\":\"ResourceReader\",\"ResultType\":11,\"ToolTip\":\"System.Resources.ResourceReader\"},{\"CompletionText\":\"System.Runtime.Versioning.ResourceScope\",\"ListItemText\":\"ResourceScope\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.Versioning.ResourceScope\"},{\"CompletionText\":\"System.Reflection.PortableExecutable.ResourceSectionBuilder\",\"ListItemText\":\"ResourceSectionBuilder\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.PortableExecutable.ResourceSectionBuilder\"},{\"CompletionText\":\"System.Resources.ResourceSet\",\"ListItemText\":\"ResourceSet\",\"ResultType\":11,\"ToolTip\":\"System.Resources.ResourceSet\"},{\"CompletionText\":\"System.Security.AccessControl.ResourceType\",\"ListItemText\":\"ResourceType\",\"ResultType\":11,\"ToolTip\":\"System.Security.AccessControl.ResourceType\"},{\"CompletionText\":\"System.Resources.ResourceWriter\",\"ListItemText\":\"ResourceWriter\",\"ResultType\":11,\"ToolTip\":\"System.Resources.ResourceWriter\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RestartComputerCommand\",\"ListItemText\":\"RestartComputerCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RestartComputerCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RestartComputerTimeoutException\",\"ListItemText\":\"RestartComputerTimeoutException\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RestartComputerTimeoutException\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.RestartServiceCommand\",\"ListItemText\":\"RestartServiceCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.RestartServiceCommand\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.InvokeRestMethodCommand+RestReturnType\",\"ListItemText\":\"RestReturnType\",\"ResultType\":11,\"ToolTip\":\"Enum Microsoft.PowerShell.Commands.InvokeRestMethodCommand+RestReturnType\"},{\"CompletionText\":\"System.DirectoryServices.Protocols.ResultCode\",\"ListItemText\":\"ResultCode\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.Protocols.ResultCode\"},{\"CompletionText\":\"System.DirectoryServices.ResultPropertyCollection\",\"ListItemText\":\"ResultPropertyCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ResultPropertyCollection\"},{\"CompletionText\":\"System.DirectoryServices.ResultPropertyValueCollection\",\"ListItemText\":\"ResultPropertyValueCollection\",\"ResultType\":11,\"ToolTip\":\"System.DirectoryServices.ResultPropertyValueCollection\"},{\"CompletionText\":\"Microsoft.PowerShell.Commands.ResumeServiceCommand\",\"ListItemText\":\"ResumeServiceCommand\",\"ResultType\":11,\"ToolTip\":\"Class Microsoft.PowerShell.Commands.ResumeServiceCommand\"},{\"CompletionText\":\"System.Data.Odbc.ODBC32+RETCODE\",\"ListItemText\":\"RETCODE\",\"ResultType\":11,\"ToolTip\":\"System.Data.Odbc.ODBC32+RETCODE\"},{\"CompletionText\":\"System.Net.Http.Headers.RetryConditionHeaderValue\",\"ListItemText\":\"RetryConditionHeaderValue\",\"ResultType\":11,\"ToolTip\":\"System.Net.Http.Headers.RetryConditionHeaderValue\"},{\"CompletionText\":\"System.Management.Automation.ReturnContainers\",\"ListItemText\":\"ReturnContainers\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.ReturnContainers\"},{\"CompletionText\":\"System.Management.Automation.Language.ReturnStatementAst\",\"ListItemText\":\"ReturnStatementAst\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Language.ReturnStatementAst\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.CSharp.Syntax.ReturnStatementSyntax\",\"ListItemText\":\"ReturnStatementSyntax\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.CSharp.Syntax.ReturnStatementSyntax\"},{\"CompletionText\":\"System.Reflection.Metadata.Ecma335.ReturnTypeEncoder\",\"ListItemText\":\"ReturnTypeEncoder\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.Metadata.Ecma335.ReturnTypeEncoder\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.IOperation+OperationList+Reversed\",\"ListItemText\":\"Reversed\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.IOperation+OperationList+Reversed\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.ChildSyntaxList+Reversed\",\"ListItemText\":\"Reversed\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.ChildSyntaxList+Reversed\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.SyntaxTriviaList+Reversed\",\"ListItemText\":\"Reversed\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.SyntaxTriviaList+Reversed\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.SyntaxTokenList+Reversed\",\"ListItemText\":\"Reversed\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.SyntaxTokenList+Reversed\"},{\"CompletionText\":\"System.Security.Cryptography.Rfc2898DeriveBytes\",\"ListItemText\":\"Rfc2898DeriveBytes\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Rfc2898DeriveBytes\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampRequest\",\"ListItemText\":\"Rfc3161TimestampRequest\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampRequest\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampToken\",\"ListItemText\":\"Rfc3161TimestampToken\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampToken\"},{\"CompletionText\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampTokenInfo\",\"ListItemText\":\"Rfc3161TimestampTokenInfo\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Pkcs.Rfc3161TimestampTokenInfo\"},{\"CompletionText\":\"System.Security.Cryptography.Rijndael\",\"ListItemText\":\"Rijndael\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Rijndael\"},{\"CompletionText\":\"System.Security.Cryptography.RijndaelManaged\",\"ListItemText\":\"RijndaelManaged\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RijndaelManaged\"},{\"CompletionText\":\"System.Security.Cryptography.RNGCryptoServiceProvider\",\"ListItemText\":\"RNGCryptoServiceProvider\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RNGCryptoServiceProvider\"},{\"CompletionText\":\"System.Management.Automation.RollbackSeverity\",\"ListItemText\":\"RollbackSeverity\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.RollbackSeverity\"},{\"CompletionText\":\"System.ComponentModel.Design.Serialization.RootDesignerSerializerAttribute\",\"ListItemText\":\"RootDesignerSerializerAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.Design.Serialization.RootDesignerSerializerAttribute\"},{\"CompletionText\":\"Roslyn\",\"ListItemText\":\"Roslyn\",\"ResultType\":10,\"ToolTip\":\"Namespace Roslyn\"},{\"CompletionText\":\"System.Drawing.RotateFlipType\",\"ListItemText\":\"RotateFlipType\",\"ResultType\":11,\"ToolTip\":\"System.Drawing.RotateFlipType\"},{\"CompletionText\":\"Markdig.Renderers.Roundtrip.Inlines.RoundtripHtmlEntityInlineRenderer\",\"ListItemText\":\"RoundtripHtmlEntityInlineRenderer\",\"ResultType\":11,\"ToolTip\":\"Markdig.Renderers.Roundtrip.Inlines.RoundtripHtmlEntityInlineRenderer\"},{\"CompletionText\":\"Markdig.Renderers.Roundtrip.Inlines.RoundtripHtmlInlineRenderer\",\"ListItemText\":\"RoundtripHtmlInlineRenderer\",\"ResultType\":11,\"ToolTip\":\"Markdig.Renderers.Roundtrip.Inlines.RoundtripHtmlInlineRenderer\"},{\"CompletionText\":\"Markdig.Renderers.Roundtrip.RoundtripObjectRenderer\",\"ListItemText\":\"RoundtripObjectRenderer<>\",\"ResultType\":11,\"ToolTip\":\"Markdig.Renderers.Roundtrip.RoundtripObjectRenderer[T]\"},{\"CompletionText\":\"Markdig.Renderers.Roundtrip.RoundtripRenderer\",\"ListItemText\":\"RoundtripRenderer\",\"ResultType\":11,\"ToolTip\":\"Markdig.Renderers.Roundtrip.RoundtripRenderer\"},{\"CompletionText\":\"JetBrains.Annotations.RouteParameterConstraintAttribute\",\"ListItemText\":\"RouteParameterConstraintAttribute\",\"ResultType\":11,\"ToolTip\":\"JetBrains.Annotations.RouteParameterConstraintAttribute\"},{\"CompletionText\":\"JetBrains.Annotations.RouteTemplateAttribute\",\"ListItemText\":\"RouteTemplateAttribute\",\"ResultType\":11,\"ToolTip\":\"JetBrains.Annotations.RouteTemplateAttribute\"},{\"CompletionText\":\"System.Data.RowNotInTableException\",\"ListItemText\":\"RowNotInTableException\",\"ResultType\":11,\"ToolTip\":\"System.Data.RowNotInTableException\"},{\"CompletionText\":\"System.Data.Common.RowUpdatedEventArgs\",\"ListItemText\":\"RowUpdatedEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.Data.Common.RowUpdatedEventArgs\"},{\"CompletionText\":\"System.Data.Common.RowUpdatingEventArgs\",\"ListItemText\":\"RowUpdatingEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.Data.Common.RowUpdatingEventArgs\"},{\"CompletionText\":\"System.Security.Cryptography.RSA\",\"ListItemText\":\"RSA\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSA\"},{\"CompletionText\":\"System.Security.Cryptography.X509Certificates.RSACertificateExtensions\",\"ListItemText\":\"RSACertificateExtensions\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.X509Certificates.RSACertificateExtensions\"},{\"CompletionText\":\"System.Security.Cryptography.RSACng\",\"ListItemText\":\"RSACng\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSACng\"},{\"CompletionText\":\"System.Security.Cryptography.RSACryptoServiceProvider\",\"ListItemText\":\"RSACryptoServiceProvider\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSACryptoServiceProvider\"},{\"CompletionText\":\"System.Security.Cryptography.RSAEncryptionPadding\",\"ListItemText\":\"RSAEncryptionPadding\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAEncryptionPadding\"},{\"CompletionText\":\"System.Security.Cryptography.RSAEncryptionPaddingMode\",\"ListItemText\":\"RSAEncryptionPaddingMode\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAEncryptionPaddingMode\"},{\"CompletionText\":\"System.Security.Cryptography.Xml.RSAKeyValue\",\"ListItemText\":\"RSAKeyValue\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.Xml.RSAKeyValue\"},{\"CompletionText\":\"System.Security.Cryptography.RSAOAEPKeyExchangeDeformatter\",\"ListItemText\":\"RSAOAEPKeyExchangeDeformatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAOAEPKeyExchangeDeformatter\"},{\"CompletionText\":\"System.Security.Cryptography.RSAOAEPKeyExchangeFormatter\",\"ListItemText\":\"RSAOAEPKeyExchangeFormatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAOAEPKeyExchangeFormatter\"},{\"CompletionText\":\"System.Security.Cryptography.RSAOpenSsl\",\"ListItemText\":\"RSAOpenSsl\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAOpenSsl\"},{\"CompletionText\":\"System.Security.Cryptography.RSAParameters\",\"ListItemText\":\"RSAParameters\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAParameters\"},{\"CompletionText\":\"System.Security.Cryptography.RSAPKCS1KeyExchangeDeformatter\",\"ListItemText\":\"RSAPKCS1KeyExchangeDeformatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAPKCS1KeyExchangeDeformatter\"},{\"CompletionText\":\"System.Security.Cryptography.RSAPKCS1KeyExchangeFormatter\",\"ListItemText\":\"RSAPKCS1KeyExchangeFormatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAPKCS1KeyExchangeFormatter\"},{\"CompletionText\":\"System.Security.Cryptography.RSAPKCS1SignatureDeformatter\",\"ListItemText\":\"RSAPKCS1SignatureDeformatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAPKCS1SignatureDeformatter\"},{\"CompletionText\":\"System.Security.Cryptography.RSAPKCS1SignatureFormatter\",\"ListItemText\":\"RSAPKCS1SignatureFormatter\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSAPKCS1SignatureFormatter\"},{\"CompletionText\":\"System.Configuration.RsaProtectedConfigurationProvider\",\"ListItemText\":\"RsaProtectedConfigurationProvider\",\"ResultType\":11,\"ToolTip\":\"System.Configuration.RsaProtectedConfigurationProvider\"},{\"CompletionText\":\"System.Security.Cryptography.RSASignaturePadding\",\"ListItemText\":\"RSASignaturePadding\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSASignaturePadding\"},{\"CompletionText\":\"System.Security.Cryptography.RSASignaturePaddingMode\",\"ListItemText\":\"RSASignaturePaddingMode\",\"ResultType\":11,\"ToolTip\":\"System.Security.Cryptography.RSASignaturePaddingMode\"},{\"CompletionText\":\"System.ServiceModel.Syndication.Rss20FeedFormatter\",\"ListItemText\":\"Rss20FeedFormatter\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Syndication.Rss20FeedFormatter\"},{\"CompletionText\":\"System.ServiceModel.Syndication.Rss20ItemFormatter\",\"ListItemText\":\"Rss20ItemFormatter\",\"ResultType\":11,\"ToolTip\":\"System.ServiceModel.Syndication.Rss20ItemFormatter\"},{\"CompletionText\":\"System.Data.Rule\",\"ListItemText\":\"Rule\",\"ResultType\":11,\"ToolTip\":\"System.Data.Rule\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RuleCache\",\"ListItemText\":\"RuleCache<>\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RuleCache[T]\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.RuleSet\",\"ListItemText\":\"RuleSet\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.RuleSet\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.RuleSetInclude\",\"ListItemText\":\"RuleSetInclude\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.RuleSetInclude\"},{\"CompletionText\":\"System.Text.Rune\",\"ListItemText\":\"Rune\",\"ResultType\":11,\"ToolTip\":\"System.Text.Rune\"},{\"CompletionText\":\"System.ComponentModel.RunInstallerAttribute\",\"ListItemText\":\"RunInstallerAttribute\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RunInstallerAttribute\"},{\"CompletionText\":\"runspace\",\"ListItemText\":\"Runspace\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.Runspace\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceAttribute\",\"ListItemText\":\"RunspaceAttribute\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceAttribute\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceAvailability\",\"ListItemText\":\"RunspaceAvailability\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspaceAvailability\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceAvailabilityEventArgs\",\"ListItemText\":\"RunspaceAvailabilityEventArgs\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceAvailabilityEventArgs\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceCapability\",\"ListItemText\":\"RunspaceCapability\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspaceCapability\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceConnectionInfo\",\"ListItemText\":\"RunspaceConnectionInfo\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceConnectionInfo\"},{\"CompletionText\":\"runspacefactory\",\"ListItemText\":\"RunspaceFactory\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceFactory\"},{\"CompletionText\":\"System.Management.Automation.RunspaceMode\",\"ListItemText\":\"RunspaceMode\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.RunspaceMode\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceOpenModuleLoadException\",\"ListItemText\":\"RunspaceOpenModuleLoadException\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceOpenModuleLoadException\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspacePool\",\"ListItemText\":\"RunspacePool\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspacePool\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspacePoolAvailability\",\"ListItemText\":\"RunspacePoolAvailability\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspacePoolAvailability\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspacePoolCapability\",\"ListItemText\":\"RunspacePoolCapability\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspacePoolCapability\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspacePoolState\",\"ListItemText\":\"RunspacePoolState\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspacePoolState\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspacePoolStateChangedEventArgs\",\"ListItemText\":\"RunspacePoolStateChangedEventArgs\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspacePoolStateChangedEventArgs\"},{\"CompletionText\":\"System.Management.Automation.RunspacePoolStateInfo\",\"ListItemText\":\"RunspacePoolStateInfo\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RunspacePoolStateInfo\"},{\"CompletionText\":\"System.Management.Automation.RunspaceRepository\",\"ListItemText\":\"RunspaceRepository\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RunspaceRepository\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceState\",\"ListItemText\":\"RunspaceState\",\"ResultType\":11,\"ToolTip\":\"Enum System.Management.Automation.Runspaces.RunspaceState\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceStateEventArgs\",\"ListItemText\":\"RunspaceStateEventArgs\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceStateEventArgs\"},{\"CompletionText\":\"System.Management.Automation.Runspaces.RunspaceStateInfo\",\"ListItemText\":\"RunspaceStateInfo\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.Runspaces.RunspaceStateInfo\"},{\"CompletionText\":\"System.RuntimeArgumentHandle\",\"ListItemText\":\"RuntimeArgumentHandle\",\"ResultType\":11,\"ToolTip\":\"System.RuntimeArgumentHandle\"},{\"CompletionText\":\"Microsoft.CSharp.RuntimeBinder.RuntimeBinderException\",\"ListItemText\":\"RuntimeBinderException\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CSharp.RuntimeBinder.RuntimeBinderException\"},{\"CompletionText\":\"Microsoft.CSharp.RuntimeBinder.RuntimeBinderInternalCompilerException\",\"ListItemText\":\"RuntimeBinderInternalCompilerException\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CSharp.RuntimeBinder.RuntimeBinderInternalCompilerException\"},{\"CompletionText\":\"Microsoft.CodeAnalysis.RuntimeCapability\",\"ListItemText\":\"RuntimeCapability\",\"ResultType\":11,\"ToolTip\":\"Microsoft.CodeAnalysis.RuntimeCapability\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RuntimeCompatibilityAttribute\",\"ListItemText\":\"RuntimeCompatibilityAttribute\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RuntimeCompatibilityAttribute\"},{\"CompletionText\":\"System.Management.Automation.RuntimeDefinedParameter\",\"ListItemText\":\"RuntimeDefinedParameter\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RuntimeDefinedParameter\"},{\"CompletionText\":\"System.Management.Automation.RuntimeDefinedParameterDictionary\",\"ListItemText\":\"RuntimeDefinedParameterDictionary\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RuntimeDefinedParameterDictionary\"},{\"CompletionText\":\"System.Runtime.InteropServices.RuntimeEnvironment\",\"ListItemText\":\"RuntimeEnvironment\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.InteropServices.RuntimeEnvironment\"},{\"CompletionText\":\"System.Management.Automation.RuntimeException\",\"ListItemText\":\"RuntimeException\",\"ResultType\":11,\"ToolTip\":\"Class System.Management.Automation.RuntimeException\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RuntimeFeature\",\"ListItemText\":\"RuntimeFeature\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RuntimeFeature\"},{\"CompletionText\":\"System.RuntimeFieldHandle\",\"ListItemText\":\"RuntimeFieldHandle\",\"ResultType\":11,\"ToolTip\":\"System.RuntimeFieldHandle\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RuntimeHelpers\",\"ListItemText\":\"RuntimeHelpers\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RuntimeHelpers\"},{\"CompletionText\":\"System.Runtime.InteropServices.RuntimeInformation\",\"ListItemText\":\"RuntimeInformation\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.InteropServices.RuntimeInformation\"},{\"CompletionText\":\"System.RuntimeMethodHandle\",\"ListItemText\":\"RuntimeMethodHandle\",\"ResultType\":11,\"ToolTip\":\"System.RuntimeMethodHandle\"},{\"CompletionText\":\"System.Reflection.RuntimeReflectionExtensions\",\"ListItemText\":\"RuntimeReflectionExtensions\",\"ResultType\":11,\"ToolTip\":\"System.Reflection.RuntimeReflectionExtensions\"},{\"CompletionText\":\"System.RuntimeTypeHandle\",\"ListItemText\":\"RuntimeTypeHandle\",\"ResultType\":11,\"ToolTip\":\"System.RuntimeTypeHandle\"},{\"CompletionText\":\"System.Linq.Expressions.RuntimeVariablesExpression\",\"ListItemText\":\"RuntimeVariablesExpression\",\"ResultType\":11,\"ToolTip\":\"System.Linq.Expressions.RuntimeVariablesExpression\"},{\"CompletionText\":\"System.Runtime.CompilerServices.RuntimeWrappedException\",\"ListItemText\":\"RuntimeWrappedException\",\"ResultType\":11,\"ToolTip\":\"System.Runtime.CompilerServices.RuntimeWrappedException\"},{\"CompletionText\":\"System.ComponentModel.RunWorkerCompletedEventArgs\",\"ListItemText\":\"RunWorkerCompletedEventArgs\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RunWorkerCompletedEventArgs\"},{\"CompletionText\":\"System.ComponentModel.RunWorkerCompletedEventHandler\",\"ListItemText\":\"RunWorkerCompletedEventHandler\",\"ResultType\":11,\"ToolTip\":\"System.ComponentModel.RunWorkerCompletedEventHandler\"}]\u0007" }, + { + "type": "input", + "data": "r" + }, { "type": "input", "data": "e" }, + { + "type": "input", + "data": "q" + }, { "type": "output", - "data": "\u001b[?25l\u001b[3;3H[re\u001b[?25h" + "data": "\u001b[?25l\u001b[3;3H[req\u001b[?25h" }, { "type": "promptInputChange", - "data": "[re|" + "data": "[req|" }, { "type": "command", @@ -99,14 +107,14 @@ export const events = [ }, { "type": "sendText", - "data": "System.Xml.Linq.ReaderOptions" + "data": "Json.Schema.RequiredKeyword" }, { "type": "output", - "data": "\u001b[?25l\u001b[3;3H[System.Xml.Linq.ReaderOptions\u001b[?25h" + "data": "\u001b[?25l\u001b[3;3H[Json.Schema.RequiredKeyword\u001b[?25h" }, { "type": "promptInputChange", - "data": "[System.Xml.Linq.ReaderOptions|" + "data": "[Json.Schema.RequiredKeyword|" } ]; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts index 90c2efa5e75..03e1ccd475b 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts @@ -3,9 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// eslint-disable-next-line local/code-import-patterns, local/code-amd-node-module -import { Terminal } from '@xterm/xterm'; - +import type { Terminal } from '@xterm/xterm'; import { strictEqual } from 'assert'; import { getActiveDocument } from 'vs/base/browser/dom'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -30,6 +28,7 @@ import { events as windows11_pwsh_namespace_completion } from 'vs/workbench/cont import { events as windows11_pwsh_type_before_prompt } from 'vs/workbench/contrib/terminalContrib/suggest/test/browser/recordings/windows11_pwsh_type_before_prompt'; import { events as windows11_pwsh_writehost_multiline_nav_up } from 'vs/workbench/contrib/terminalContrib/suggest/test/browser/recordings/windows11_pwsh_writehost_multiline_nav_up'; import { events as windows11_pwsh_writehost_multiline } from 'vs/workbench/contrib/terminalContrib/suggest/test/browser/recordings/windows11_pwsh_writehost_multiline'; +import { importAMDNodeModule } from 'vs/amdX'; const recordedTestCases: { name: string; events: RecordedSessionEvent[] }[] = [ { name: 'macos_bash_echo_simple', events: macos_bash_echo_simple as any as RecordedSessionEvent[] }, @@ -69,7 +68,7 @@ suite('Terminal Contrib Suggest Recordings', () => { let suggestWidgetVisibleContextKey: IContextKey; let suggestAddon: SuggestAddon; - setup(() => { + setup(async () => { const instantiationService = workbenchInstantiationService({ configurationService: () => new TestConfigurationService({ files: { autoSave: false }, @@ -84,7 +83,8 @@ suite('Terminal Contrib Suggest Recordings', () => { } }) }, store); - xterm = store.add(new Terminal({ allowProposedApi: true })); + const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; + xterm = store.add(new TerminalCtor({ allowProposedApi: true })); const shellIntegrationAddon = store.add(new ShellIntegrationAddon('', true, undefined, new NullLogService)); capabilities = shellIntegrationAddon.capabilities; suggestWidgetVisibleContextKey = TerminalContextKeys.suggestWidgetVisible.bindTo(instantiationService.get(IContextKeyService)); diff --git a/src/vs/workbench/contrib/terminalContrib/typeAhead/test/browser/terminalTypeAhead.test.ts b/src/vs/workbench/contrib/terminalContrib/typeAhead/test/browser/terminalTypeAhead.test.ts index 86972f65fa7..14f61b9868a 100644 --- a/src/vs/workbench/contrib/terminalContrib/typeAhead/test/browser/terminalTypeAhead.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/typeAhead/test/browser/terminalTypeAhead.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import type { IBuffer, Terminal } from '@xterm/xterm'; import { SinonStub, stub, useFakeTimers } from 'sinon'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index 67cc55a485d..61806a01f25 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -16,7 +16,7 @@ import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { autorun, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; +import { autorun, derived, observableFromEvent } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; import { isUriComponents, URI } from 'vs/base/common/uri'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; @@ -51,7 +51,6 @@ import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { CoverageDetails, DetailType, IDeclarationCoverage, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; -const MAX_HOVERED_LINES = 30; const CLASS_HIT = 'coverage-deco-hit'; const CLASS_MISS = 'coverage-deco-miss'; const TOGGLE_INLINE_COMMAND_TEXT = localize('testing.toggleInlineCoverage', 'Toggle Inline'); @@ -59,9 +58,6 @@ const TOGGLE_INLINE_COMMAND_ID = 'testing.toggleInlineCoverage'; const BRANCH_MISS_INDICATOR_CHARS = 4; export class CodeCoverageDecorations extends Disposable implements IEditorContribution { - public static showInline = observableValue('inlineCoverage', false); - private static readonly fileCoverageDecorations = new WeakMap(); - private loadingCancellation?: CancellationTokenSource; private readonly displayedStore = this._register(new DisposableStore()); private readonly hoveredStore = this._register(new DisposableStore()); @@ -77,7 +73,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri constructor( private readonly editor: ICodeEditor, @IInstantiationService instantiationService: IInstantiationService, - @ITestCoverageService coverage: ITestCoverageService, + @ITestCoverageService private readonly coverage: ITestCoverageService, @IConfigurationService configurationService: IConfigurationService, @ILogService private readonly log: ILogService, ) { @@ -85,8 +81,8 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri this.summaryWidget = new Lazy(() => this._register(instantiationService.createInstance(CoverageToolbarWidget, this.editor))); - const modelObs = observableFromEvent(editor.onDidChangeModel, () => editor.getModel()); - const configObs = observableFromEvent(editor.onDidChangeConfiguration, i => i); + const modelObs = observableFromEvent(this, editor.onDidChangeModel, () => editor.getModel()); + const configObs = observableFromEvent(this, editor.onDidChangeConfiguration, i => i); const fileCoverage = derived(reader => { const report = coverage.selected.read(reader); @@ -99,24 +95,19 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri return; } - let file = report.getUri(model.uri); - if (file) { - const testFilter = coverage.filterToTest.read(reader); - if (testFilter) { - file = file.perTestData?.get(testFilter.toString()) || file; - } - - return file; + const file = report.getUri(model.uri); + if (!file) { + return; } report.didAddCoverage.read(reader); // re-read if changes when there's no report - return undefined; + return { file, testId: coverage.filterToTest.read(reader) }; }); this._register(autorun(reader => { const c = fileCoverage.read(reader); if (c) { - this.apply(editor.getModel()!, c, CodeCoverageDecorations.showInline.read(reader)); + this.apply(editor.getModel()!, c.file, c.testId, coverage.showInline.read(reader)); } else { this.clear(); } @@ -126,9 +117,9 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri this._register(autorun(reader => { const c = fileCoverage.read(reader); if (c && toolbarEnabled.read(reader)) { - this.summaryWidget.value.setCoverage(c); + this.summaryWidget.value.setCoverage(c.file, c.testId); } else { - this.summaryWidget.rawValue?.setCoverage(undefined); + this.summaryWidget.rawValue?.clearCoverage(); } })); @@ -146,7 +137,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri const model = editor.getModel(); if (e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS && model) { this.hoverLineNumber(editor.getModel()!, e.target.position.lineNumber); - } else if (CodeCoverageDecorations.showInline.get() && e.target.type === MouseTargetType.CONTENT_TEXT && model) { + } else if (coverage.showInline.get() && e.target.type === MouseTargetType.CONTENT_TEXT && model) { this.hoverInlineDecoration(model, e.target.position); } else { this.hoveredStore.clear(); @@ -214,9 +205,14 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri const todo = [{ line: lineNumber, dir: 0 }]; const toEnable = new Set(); - if (!CodeCoverageDecorations.showInline.get()) { - for (let i = 0; i < todo.length && i < MAX_HOVERED_LINES; i++) { + const ranges = this.editor.getVisibleRanges(); + if (!this.coverage.showInline.get()) { + for (let i = 0; i < todo.length; i++) { const { line, dir } = todo[i]; + if (!ranges.some(r => r.startLineNumber <= line && r.endLineNumber >= line)) { + continue; // stop once outside the viewport + } + let found = false; for (const decoration of model.getLineDecorations(line)) { if (this.decorationIds.has(decoration.id)) { @@ -262,8 +258,8 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri })); } - private async apply(model: ITextModel, coverage: FileCoverage, showInlineByDefault: boolean) { - const details = this.details = await this.loadDetails(coverage, model); + private async apply(model: ITextModel, coverage: FileCoverage, testId: TestId | undefined, showInlineByDefault: boolean) { + const details = this.details = await this.loadDetails(coverage, testId, model); if (!details) { return this.clear(); } @@ -348,24 +344,18 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri this.hoveredStore.clear(); } - private async loadDetails(coverage: FileCoverage, textModel: ITextModel) { - const existing = CodeCoverageDecorations.fileCoverageDecorations.get(coverage); - if (existing) { - return existing; - } - + private async loadDetails(coverage: FileCoverage, testId: TestId | undefined, textModel: ITextModel) { const cts = this.loadingCancellation = new CancellationTokenSource(); this.displayedStore.add(this.loadingCancellation); try { - const details = await coverage.details(this.loadingCancellation.token); + const details = testId + ? await coverage.detailsForTest(testId, this.loadingCancellation.token) + : await coverage.details(this.loadingCancellation.token); if (cts.token.isCancellationRequested) { return; } - const model = CodeCoverageDecorations.fileCoverageDecorations.get(coverage) - || new CoverageDetailsModel(details, textModel); - CodeCoverageDecorations.fileCoverageDecorations.set(coverage, model); - return model; + return new CoverageDetailsModel(details, textModel); } catch (e) { this.log.error('Error loading coverage details', e); } @@ -535,7 +525,7 @@ function wrapName(functionNameOrCode: string) { } class CoverageToolbarWidget extends Disposable implements IOverlayWidget { - private current: FileCoverage | undefined; + private current: { coverage: FileCoverage; testId: TestId | undefined } | undefined; private registered = false; private isRunning = false; private readonly showStore = this._register(new DisposableStore()); @@ -556,6 +546,7 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { @ITestService private readonly testService: ITestService, @IKeybindingService private readonly keybindingService: IKeybindingService, @ICommandService private readonly commandService: ICommandService, + @ITestCoverageService private readonly coverage: ITestCoverageService, @IInstantiationService instaService: IInstantiationService, ) { super(); @@ -579,7 +570,7 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { this._register(autorun(reader => { - CodeCoverageDecorations.showInline.read(reader); + coverage.showInline.read(reader); this.setActions(); })); @@ -609,8 +600,14 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { }; } - public setCoverage(coverage: FileCoverage | undefined) { - this.current = coverage; + public clearCoverage() { + this.current = undefined; + this.bars.setCoverageInfo(undefined); + this.hide(); + } + + public setCoverage(coverage: FileCoverage, testId: TestId | undefined) { + this.current = { coverage, testId }; this.bars.setCoverageInfo(coverage); if (!coverage) { @@ -623,19 +620,19 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { private setActions() { this.actionBar.clear(); - const coverage = this.current; - if (!coverage) { + const current = this.current; + if (!current) { return; } const toggleAction = new ActionWithIcon( 'toggleInline', - CodeCoverageDecorations.showInline.get() + this.coverage.showInline.get() ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') : localize('testing.showInlineCoverage', 'Show Inline Coverage'), testingCoverageReport, undefined, - () => CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined), + () => this.coverage.showInline.set(!this.coverage.showInline.get(), undefined), ); const kb = this.keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); @@ -645,21 +642,21 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { this.actionBar.push(toggleAction); - if (coverage.isForTest) { - const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.toString()); + if (current.testId) { + const testItem = current.coverage.fromResult.getTestById(current.testId.toString()); assert(!!testItem, 'got coverage for an unreported test'); this.actionBar.push(new ActionWithIcon('perTestFilter', coverUtils.labels.showingFilterFor(testItem.label), testingFilterIcon, undefined, - () => this.commandService.executeCommand(TestCommandId.CoverageFilterToTestInEditor, this.current), + () => this.commandService.executeCommand(TestCommandId.CoverageFilterToTestInEditor, this.current, this.editor), )); - } else if (coverage.perTestData?.size) { + } else if (current.coverage.perTestData?.size) { this.actionBar.push(new ActionWithIcon('perTestFilter', - localize('testing.coverageForTestAvailable', "{0} test(s) ran code in this file", coverage.perTestData.size), + localize('testing.coverageForTestAvailable', "{0} test(s) ran code in this file", current.coverage.perTestData.size), testingFilterIcon, undefined, - () => this.commandService.executeCommand(TestCommandId.CoverageFilterToTestInEditor, this.current), + () => this.commandService.executeCommand(TestCommandId.CoverageFilterToTestInEditor, this.current, this.editor), )); } @@ -701,8 +698,8 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { })); ds.add(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TestingConfigKeys.CoverageBarThresholds) || e.affectsConfiguration(TestingConfigKeys.CoveragePercent)) { - this.setCoverage(this.current); + if (this.current && (e.affectsConfiguration(TestingConfigKeys.CoverageBarThresholds) || e.affectsConfiguration(TestingConfigKeys.CoveragePercent))) { + this.setCoverage(this.current.coverage, this.current.testId); } })); } @@ -712,7 +709,7 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { if (current) { this.isRunning = true; this.setActions(); - this.testService.runResolvedTests(current.fromResult.request).finally(() => { + this.testService.runResolvedTests(current.coverage.fromResult.request).finally(() => { this.isRunning = false; this.setActions(); }); @@ -728,12 +725,18 @@ registerAction2(class ToggleInlineCoverage extends Action2 { constructor() { super({ id: TOGGLE_INLINE_COMMAND_ID, - title: localize2('coverage.toggleInline', "Show Inline Coverage"), + // note: ideally this would be "show inline", but the command palette does + // not use the 'toggled' titles, so we need to make this generic. + title: localize2('coverage.toggleInline', "Toggle Inline Coverage"), category: Categories.Test, keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI), }, + toggled: { + condition: TestingContextKeys.inlineCoverageEnabled, + title: localize('coverage.hideInline', "Hide Inline Coverage"), + }, icon: testingCoverageReport, menu: [ { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, @@ -742,8 +745,9 @@ registerAction2(class ToggleInlineCoverage extends Action2 { }); } - public run() { - CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); + public run(accessor: ServicesAccessor): void { + const coverage = accessor.get(ITestCoverageService); + coverage.showInline.set(!coverage.showInline.get(), undefined); } }); @@ -791,10 +795,10 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { }); } - run(accessor: ServicesAccessor, coverageOrUri?: FileCoverage | URI): void { + run(accessor: ServicesAccessor, coverageOrUri?: FileCoverage | URI, editor?: ICodeEditor): void { const testCoverageService = accessor.get(ITestCoverageService); const quickInputService = accessor.get(IQuickInputService); - const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor(); + const activeEditor = editor ?? accessor.get(ICodeEditorService).getActiveCodeEditor(); let coverage: FileCoverage | undefined; if (coverageOrUri instanceof FileCoverage) { coverage = coverageOrUri; @@ -805,26 +809,21 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { coverage = uri && testCoverageService.selected.get()?.getUri(uri); } - if (!coverage || !(coverage.isForTest || coverage.perTestData?.size)) { + if (!coverage || !coverage.perTestData?.size) { return; } - const options = coverage?.perTestData ?? coverage?.isForTest?.parent.perTestData; - if (!options) { - return; - } - - const tests = [...options.values()]; - const commonPrefix = TestId.getLengthOfCommonPrefix(tests.length, i => tests[i].isForTest!.id); + const tests = [...coverage.perTestData].map(TestId.fromString); + const commonPrefix = TestId.getLengthOfCommonPrefix(tests.length, i => tests[i]); const result = coverage.fromResult; const previousSelection = testCoverageService.filterToTest.get(); - type TItem = { label: string; description?: string; item: FileCoverage | undefined }; + type TItem = { label: string; testId: TestId | undefined }; const items: QuickPickInput[] = [ - { label: coverUtils.labels.allTests, item: undefined }, + { label: coverUtils.labels.allTests, testId: undefined }, { type: 'separator' }, - ...tests.map(item => ({ label: coverUtils.getLabelForItem(result, item.isForTest!.id, commonPrefix), description: coverUtils.labels.percentCoverage(item.tpc), item })), + ...tests.map(id => ({ label: coverUtils.getLabelForItem(result, id, commonPrefix), testId: id })), ]; // These handle the behavior that reveals the start of coverage when the @@ -837,13 +836,13 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { activeItem: items.find((item): item is TItem => 'item' in item && item.item === coverage), placeHolder: coverUtils.labels.pickShowCoverage, onDidFocus: (entry) => { - if (!entry.item) { + if (!entry.testId) { revealScrollCts.clear(); activeEditor?.setScrollTop(scrollTop); testCoverageService.filterToTest.set(undefined, undefined); } else { const cts = revealScrollCts.value = new CancellationTokenSource(); - entry.item.details(cts.token).then( + coverage.detailsForTest(entry.testId, cts.token).then( details => { const first = details.find(d => d.type === DetailType.Statement); if (!cts.token.isCancellationRequested && first) { @@ -852,7 +851,7 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { }, () => { /* ignored */ } ); - testCoverageService.filterToTest.set(entry.item.isForTest!.id, undefined); + testCoverageService.filterToTest.set(entry.testId, undefined); } }, }).then(selected => { @@ -861,7 +860,7 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { } revealScrollCts.dispose(); - testCoverageService.filterToTest.set(selected ? selected.item?.isForTest!.id : previousSelection, undefined); + testCoverageService.filterToTest.set(selected ? selected.testId : previousSelection, undefined); }); } }); diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts index 33060d117ea..9e752e5f4ef 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts @@ -223,9 +223,11 @@ export class TreeProjection extends Disposable implements ITestTreeProjection { break; } - // The first element will cause the root to be hidden + // Removing the first element will cause the root to be hidden. + // Changing first-level elements will need the root to re-render if + // there are no other controllers with items. const parent = toRemove.parent; - const affectsRootElement = toRemove.depth === 1 && parent?.children.size === 1; + const affectsRootElement = toRemove.depth === 1 && (parent?.children.size === 1 || !Iterable.some(this.rootsWithChildren, (_, i) => i === 1)); this.changedParents.add(affectsRootElement ? null : parent); const queue: Iterable[] = [[toRemove]]; diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts b/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts index 3e4cf9e7131..232184a9e5b 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { h } from 'vs/base/browser/dom'; -import type { IUpdatableHover, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover, IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; @@ -18,7 +18,6 @@ import { IHoverService } from 'vs/platform/hover/browser/hover'; import { Registry } from 'vs/platform/registry/common/platform'; import { ExplorerExtensions, IExplorerFileContribution, IExplorerFileContributionRegistry } from 'vs/workbench/contrib/files/browser/explorerFileContrib'; import * as coverUtils from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; -import { calculateDisplayedStat } from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; import { ITestingCoverageBarThresholds, TestingConfigKeys, getTestingConfiguration, observeTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; import { AbstractFileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; @@ -67,7 +66,7 @@ export class ManagedTestCoverageBars extends Disposable { }); private readonly visibleStore = this._register(new DisposableStore()); - private readonly customHovers: IUpdatableHover[] = []; + private readonly customHovers: IManagedHover[] = []; /** Gets whether coverage is currently visible for the resource. */ public get visible() { @@ -82,8 +81,8 @@ export class ManagedTestCoverageBars extends Disposable { super(); } - private attachHover(target: HTMLElement, factory: (coverage: CoverageBarSource) => string | IUpdatableHoverTooltipMarkdownString | undefined) { - this._register(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), target, () => this._coverage && factory(this._coverage))); + private attachHover(target: HTMLElement, factory: (coverage: CoverageBarSource) => string | IManagedHoverTooltipMarkdownString | undefined) { + this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), target, () => this._coverage && factory(this._coverage))); } public setCoverageInfo(coverage: CoverageBarSource | undefined) { @@ -99,7 +98,7 @@ export class ManagedTestCoverageBars extends Disposable { if (!this._coverage) { const root = this.el.value.root; - ds.add(toDisposable(() => this.options.container.removeChild(root))); + ds.add(toDisposable(() => root.remove())); this.options.container.appendChild(root); ds.add(this.configurationService.onDidChangeConfiguration(c => { if (!this._coverage) { @@ -121,7 +120,7 @@ export class ManagedTestCoverageBars extends Disposable { const precision = this.options.compact ? 0 : 2; const thresholds = getTestingConfiguration(this.configurationService, TestingConfigKeys.CoverageBarThresholds); - const overallStat = calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent)); + const overallStat = coverUtils.calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent)); if (this.options.overall !== false) { el.overall.textContent = coverUtils.displayPercent(overallStat, precision); } else { @@ -165,7 +164,7 @@ const stmtCoverageText = (coverage: CoverageBarSource) => localize('statementCov const fnCoverageText = (coverage: CoverageBarSource) => coverage.declaration && localize('functionCoverage', '{0}/{1} functions covered ({2})', nf.format(coverage.declaration.covered), nf.format(coverage.declaration.total), coverUtils.displayPercent(coverUtils.percent(coverage.declaration))); const branchCoverageText = (coverage: CoverageBarSource) => coverage.branch && localize('branchCoverage', '{0}/{1} branches covered ({2})', nf.format(coverage.branch.covered), nf.format(coverage.branch.total), coverUtils.displayPercent(coverUtils.percent(coverage.branch))); -const getOverallHoverText = (coverage: CoverageBarSource): IUpdatableHoverTooltipMarkdownString => { +const getOverallHoverText = (coverage: CoverageBarSource): IManagedHoverTooltipMarkdownString => { const str = [ stmtCoverageText(coverage), fnCoverageText(coverage), diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index 8e67eebd282..26ecefd409a 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -643,7 +643,7 @@ registerAction2(class TestCoverageChangePerTestFilterAction extends Action2 { return; } - const tests = [...coverage.perTestCoverageIDs].map(TestId.fromString); + const tests = [...coverage.allPerTestIDs()].map(TestId.fromString); const commonPrefix = TestId.getLengthOfCommonPrefix(tests.length, i => tests[i]); const result = coverage.result; const previousSelection = coverageService.filterToTest.get(); diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 6851abd333b..37cb8843ff5 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -44,7 +44,7 @@ import { ITestProfileService, canUseProfileWithTest } from 'vs/workbench/contrib import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { IMainThreadTestCollection, IMainThreadTestController, ITestService, expandAndGetTestById, testsInFile, testsUnderUri } from 'vs/workbench/contrib/testing/common/testService'; -import { ExtTestRunProfileKind, ITestRunProfile, InternalTestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; +import { ExtTestRunProfileKind, ITestRunProfile, InternalTestItem, TestItemExpandState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; @@ -224,8 +224,8 @@ export class RunUsingProfileAction extends Action2 { } testService.runResolvedTests({ + group: profile.group, targets: [{ - profileGroup: profile.group, profileId: profile.profileId, controllerId: profile.controllerId, testIds: elements.filter(t => canUseProfileWithTest(profile, t.test)).map(t => t.test.item.extId) @@ -625,7 +625,8 @@ abstract class RunOrDebugAllTestsAction extends Action2 { const testService = accessor.get(ITestService); const notifications = accessor.get(INotificationService); - const roots = [...testService.collection.rootItems]; + const roots = [...testService.collection.rootItems].filter(r => r.children.size + || r.expand === TestItemExpandState.Expandable || r.expand === TestItemExpandState.BusyExpanding); if (!roots.length) { notifications.info(this.noTestsFoundError); return; @@ -1345,7 +1346,8 @@ abstract class RunOrDebugFailedTests extends RunOrDebugExtsByPath { } } -abstract class RunOrDebugLastRun extends RunOrDebugExtsByPath { + +abstract class RunOrDebugLastRun extends Action2 { constructor(options: IAction2Options) { super({ ...options, @@ -1359,21 +1361,46 @@ abstract class RunOrDebugLastRun extends RunOrDebugExtsByPath { }); } - /** - * @inheritdoc - */ - protected *getTestExtIdsToRun(accessor: ServicesAccessor, runId?: string): Iterable { + protected abstract getGroup(): TestRunProfileBitset; + + protected getLastTestRunRequest(accessor: ServicesAccessor, runId?: string) { + const resultService = accessor.get(ITestResultService); + const lastResult = runId ? resultService.results.find(r => r.id === runId) : resultService.results[0]; + return lastResult?.request; + } + + /** @inheritdoc */ + public override async run(accessor: ServicesAccessor, runId?: string) { const resultService = accessor.get(ITestResultService); const lastResult = runId ? resultService.results.find(r => r.id === runId) : resultService.results[0]; if (!lastResult) { return; } - for (const test of lastResult.request.targets) { - for (const testId of test.testIds) { - yield testId; - } - } + const req = lastResult.request; + const testService = accessor.get(ITestService); + const profileService = accessor.get(ITestProfileService); + const profileExists = (t: { controllerId: string; profileId: number }) => + profileService.getControllerProfiles(t.controllerId).some(p => p.profileId === t.profileId); + + await discoverAndRunTests( + testService.collection, + accessor.get(IProgressService), + req.targets.flatMap(t => t.testIds), + tests => { + // If we're requesting a re-run in the same group and have the same profiles + // as were used before, then use those exactly. Otherwise guess naively. + if (this.getGroup() & req.group && req.targets.every(profileExists)) { + return testService.runResolvedTests({ + targets: req.targets, + group: req.group, + exclude: req.exclude, + }); + } else { + return testService.runTests({ tests, group: this.getGroup() }); + } + }, + ); } } @@ -1432,11 +1459,8 @@ export class ReRunLastRun extends RunOrDebugLastRun { }); } - protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { - return service.runTests({ - group: TestRunProfileBitset.Run, - tests: internalTests, - }); + protected override getGroup(): TestRunProfileBitset { + return TestRunProfileBitset.Run; } } @@ -1453,11 +1477,8 @@ export class DebugLastRun extends RunOrDebugLastRun { }); } - protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { - return service.runTests({ - group: TestRunProfileBitset.Debug, - tests: internalTests, - }); + protected override getGroup(): TestRunProfileBitset { + return TestRunProfileBitset.Debug; } } @@ -1474,11 +1495,8 @@ export class CoverageLastRun extends RunOrDebugLastRun { }); } - protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise { - return service.runTests({ - group: TestRunProfileBitset.Coverage, - tests: internalTests, - }); + protected override getGroup(): TestRunProfileBitset { + return TestRunProfileBitset.Coverage; } } diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index b2742e2480d..80f6674801a 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -47,7 +47,7 @@ import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { ITestService, getContextForTestItem, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; +import { ITestService, getContextForTestItem, simplifyTestsToExecute, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; import { IRichLocation, ITestMessage, ITestRunProfile, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestDecoration as IPublicTestDecoration, ITestingDecorationsService, TestDecorations } from 'vs/workbench/contrib/testing/common/testingDecorations'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; @@ -806,7 +806,7 @@ abstract class RunTestDecoration { protected runWith(profile: TestRunProfileBitset) { return this.testService.runTests({ - tests: this.tests.map(({ test }) => test), + tests: simplifyTestsToExecute(this.testService.collection, this.tests.map(({ test }) => test)), group: profile, }); } @@ -856,8 +856,8 @@ abstract class RunTestDecoration { } this.testService.runResolvedTests({ + group: profile.group, targets: [{ - profileGroup: profile.group, profileId: profile.profileId, controllerId: profile.controllerId, testIds: [test.item.extId] @@ -880,16 +880,12 @@ abstract class RunTestDecoration { private getContributedTestActions(test: InternalTestItem, capabilities: number): IAction[] { const contextOverlay = this.contextKeyService.createOverlay(getTestItemContextOverlay(test, capabilities)); - const menu = this.menuService.createMenu(MenuId.TestItemGutter, contextOverlay); - try { - const target: IAction[] = []; - const arg = getContextForTestItem(this.testService.collection, test.item.extId); - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true, arg }, target); - return target; - } finally { - menu.dispose(); - } + const target: IAction[] = []; + const arg = getContextForTestItem(this.testService.collection, test.item.extId); + const menu = this.menuService.getMenuActions(MenuId.TestItemGutter, contextOverlay, { shouldForwardArgs: true, arg }); + createAndFillInContextMenuActions(menu, target); + return target; } } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 6f90138b102..b4a2281c215 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -28,6 +28,7 @@ const testFilterDescriptions: { [K in TestFilterTerm]: string } = { [TestFilterTerm.Failed]: localize('testing.filters.showOnlyFailed', "Show Only Failed Tests"), [TestFilterTerm.Executed]: localize('testing.filters.showOnlyExecuted', "Show Only Executed Tests"), [TestFilterTerm.CurrentDoc]: localize('testing.filters.currentFile', "Show in Active File Only"), + [TestFilterTerm.OpenedFiles]: localize('testing.filters.openedFiles', "Show in Opened Files Only"), [TestFilterTerm.Hidden]: localize('testing.filters.showExcludedTests', "Show Hidden Tests"), }; @@ -201,7 +202,7 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { private getActions(): IAction[] { return [ - ...[TestFilterTerm.Failed, TestFilterTerm.Executed, TestFilterTerm.CurrentDoc].map(term => ({ + ...[TestFilterTerm.Failed, TestFilterTerm.Executed, TestFilterTerm.CurrentDoc, TestFilterTerm.OpenedFiles].map(term => ({ checked: this.filters.isFilteringFor(term), class: undefined, enabled: true, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 5c5572d1dc0..7bf8db331eb 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -8,7 +8,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ActionBar, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; -import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; +import type { IManagedHover } from 'vs/base/browser/ui/hover/hover'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; @@ -22,6 +22,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { autorun, observableFromEvent } from 'vs/base/common/observable'; import { fuzzyContains } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { isDefined } from 'vs/base/common/types'; @@ -30,7 +31,7 @@ import 'vs/css!./media/testing'; import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; import { localize } from 'vs/nls'; import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; -import { MenuEntryActionViewItem, createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { MenuEntryActionViewItem, createActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -78,6 +79,7 @@ import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/commo import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { cmpPriority, isFailedState, isStateWithResult, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates'; import { IActivityService, IconBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; const enum LastFocusState { @@ -115,6 +117,7 @@ export class TestingExplorerView extends ViewPane { @IHoverService hoverService: IHoverService, @ITestProfileService private readonly testProfileService: ITestProfileService, @ICommandService private readonly commandService: ICommandService, + @IMenuService private readonly menuService: IMenuService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); @@ -337,8 +340,8 @@ export class TestingExplorerView extends ViewPane { const { include, exclude } = this.getTreeIncludeExclude(undefined, profile); this.testService.runResolvedTests({ exclude: exclude.map(e => e.item.extId), + group: profile.group, targets: [{ - profileGroup: profile.group, profileId: profile.profileId, controllerId: profile.controllerId, testIds: include.map(i => i.item.extId), @@ -349,10 +352,23 @@ export class TestingExplorerView extends ViewPane { } } - // If there's only one group, don't add a heading for it in the dropdown. - if (participatingGroups === 1) { - profileActions.shift(); + const menuActions: IAction[] = []; + const contextKeys: [string, unknown][] = []; + // allow extension author to define context for when to show the test menu actions for run or debug menus + if (group === TestRunProfileBitset.Run) { + contextKeys.push(['testing.profile.context.group', 'run']); } + if (group === TestRunProfileBitset.Debug) { + contextKeys.push(['testing.profile.context.group', 'debug']); + } + if (group === TestRunProfileBitset.Coverage) { + contextKeys.push(['testing.profile.context.group', 'coverage']); + } + const key = this.contextKeyService.createOverlay(contextKeys); + const menu = this.menuService.getMenuActions(MenuId.TestProfilesContext, key); + + // fill if there are any actions + createAndFillInContextMenuActions(menu, menuActions); const postActions: IAction[] = []; if (profileActions.length > 1) { @@ -375,7 +391,10 @@ export class TestingExplorerView extends ViewPane { )); } - return Separator.join(profileActions, postActions); + // show menu actions if there are any otherwise don't + return menuActions.length > 0 + ? Separator.join(profileActions, menuActions, postActions) + : Separator.join(profileActions, postActions); } /** @@ -448,7 +467,7 @@ class ResultSummaryView extends Disposable { private elementsWereAttached = false; private badgeType: TestingCountBadge; private lastBadge?: NumberBadge | IconBadge; - private countHover: IUpdatableHover; + private countHover: IManagedHover; private readonly badgeDisposable = this._register(new MutableDisposable()); private readonly renderLoop = this._register(new RunOnceScheduler(() => this.render(), SUMMARY_RENDER_INTERVAL)); private readonly elements = dom.h('div.result-summary', [ @@ -480,7 +499,7 @@ class ResultSummaryView extends Disposable { } })); - this.countHover = this._register(hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), this.elements.count, '')); + this.countHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.elements.count, '')); const ab = this._register(new ActionBar(this.elements.rerun, { actionViewItemProvider: (action, options) => createActionViewItem(instantiationService, action, options), @@ -500,7 +519,7 @@ class ResultSummaryView extends Disposable { const { count, root, status, duration, rerun } = this.elements; if (!results.length) { if (this.elementsWereAttached) { - this.container.removeChild(root); + root.remove(); this.elementsWereAttached = false; } this.container.innerText = localize('noResults', 'No test results yet.'); @@ -648,6 +667,7 @@ class TestingExplorerViewModel extends Disposable { onDidChangeVisibility: Event, @IConfigurationService configurationService: IConfigurationService, @IEditorService editorService: IEditorService, + @IEditorGroupsService editorGroupsService: IEditorGroupsService, @IMenuService private readonly menuService: IMenuService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @ITestService private readonly testService: ITestService, @@ -818,27 +838,38 @@ class TestingExplorerViewModel extends Disposable { this.tree.rerender(); })); - const onEditorChange = () => { + const allOpenEditorInputs = observableFromEvent(this, + editorService.onDidEditorsChange, + () => new Set(editorGroupsService.groups.flatMap(g => g.editors).map(e => e.resource).filter(isDefined)), + ); + + const activeResource = observableFromEvent(this, editorService.onDidActiveEditorChange, () => { if (editorService.activeEditor instanceof DiffEditorInput) { - this.filter.filterToDocumentUri(editorService.activeEditor.primary.resource); + return editorService.activeEditor.primary.resource; } else { - this.filter.filterToDocumentUri(editorService.activeEditor?.resource); + return editorService.activeEditor?.resource; + } + }); + + const filterText = observableFromEvent(this.filterState.text.onDidChange, () => this.filterState.text); + this._register(autorun(reader => { + filterText.read(reader); + if (this.filterState.isFilteringFor(TestFilterTerm.OpenedFiles)) { + this.filter.filterToDocumentUri([...allOpenEditorInputs.read(reader)]); + } else { + this.filter.filterToDocumentUri([activeResource.read(reader)].filter(isDefined)); } - if (this.filterState.isFilteringFor(TestFilterTerm.CurrentDoc)) { + if (this.filterState.isFilteringFor(TestFilterTerm.CurrentDoc) || this.filterState.isFilteringFor(TestFilterTerm.OpenedFiles)) { this.tree.refilter(); } - }; - - this._register(editorService.onDidActiveEditorChange(onEditorChange)); + })); this._register(this.storageService.onWillSaveState(({ reason, }) => { if (reason === WillSaveStateReason.SHUTDOWN) { this.lastViewState.store(this.tree.getOptimizedViewState()); } })); - - onEditorChange(); } /** @@ -1067,7 +1098,7 @@ const hasNodeInOrParentOfUri = (collection: IMainThreadTestCollection, ident: IU }; class TestsFilter implements ITreeFilter { - private documentUri: URI | undefined; + private documentUris: URI[] = []; constructor( private readonly collection: IMainThreadTestCollection, @@ -1102,8 +1133,8 @@ class TestsFilter implements ITreeFilter { } } - public filterToDocumentUri(uri: URI | undefined) { - this.documentUri = uri; + public filterToDocumentUri(uris: readonly URI[]) { + this.documentUris = [...uris]; } private testTags(element: TestItemTreeElement): FilterResult { @@ -1131,15 +1162,15 @@ class TestsFilter implements ITreeFilter { } private testLocation(element: TestItemTreeElement): FilterResult { - if (!this.documentUri) { + if (this.documentUris.length === 0) { return FilterResult.Include; } - if (!this.state.isFilteringFor(TestFilterTerm.CurrentDoc) || !(element instanceof TestItemTreeElement)) { + if ((!this.state.isFilteringFor(TestFilterTerm.CurrentDoc) && !this.state.isFilteringFor(TestFilterTerm.OpenedFiles)) || !(element instanceof TestItemTreeElement)) { return FilterResult.Include; } - if (hasNodeInOrParentOfUri(this.collection, this.uriIdentityService, this.documentUri, element.test.item.extId)) { + if (this.documentUris.some(uri => hasNodeInOrParentOfUri(this.collection, this.uriIdentityService, uri, element.test.item.extId))) { return FilterResult.Include; } @@ -1344,7 +1375,7 @@ class ErrorRenderer implements ITreeRenderer { diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index a05b7d50fe0..ec21581a9a6 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -780,6 +780,7 @@ class FollowupActionWidget extends Disposable { constructor( private readonly container: HTMLElement, + private readonly editor: ICodeEditor | undefined, @ITestService private readonly testService: ITestService, @IQuickInputService private readonly quickInput: IQuickInputService, ) { @@ -827,7 +828,7 @@ class FollowupActionWidget extends Disposable { this.container.appendChild(this.el.root); this.visibleStore.add(toDisposable(() => { - this.el.root.parentElement?.removeChild(this.el.root); + this.el.root.remove(); })); } @@ -871,6 +872,10 @@ class FollowupActionWidget extends Disposable { if (link.ariaDisabled !== 'true') { link.ariaDisabled = 'true'; fu.execute(); + + if (this.editor) { + TestingOutputPeekController.get(this.editor)?.removePeek(); + } } } } @@ -917,7 +922,7 @@ class TestResultsViewContent extends Disposable { const { historyVisible, showRevealLocationOnMessages } = this.options; const isInPeekView = this.editor !== undefined; const messageContainer = this.messageContainer = dom.append(containerElement, dom.$('.test-output-peek-message-container')); - this.followupWidget = this._register(this.instantiationService.createInstance(FollowupActionWidget, messageContainer)); + this.followupWidget = this._register(this.instantiationService.createInstance(FollowupActionWidget, messageContainer, this.editor)); this.contentProviders = [ this._register(this.instantiationService.createInstance(DiffContentProvider, this.editor, messageContainer)), this._register(this.instantiationService.createInstance(MarkdownTestMessagePeek, messageContainer)), @@ -1020,15 +1025,14 @@ class TestResultsViewContent extends Disposable { } - this.currentSubjectStore.add( - this.instantiationService - .createChild(new ServiceCollection([IContextKeyService, this.messageContextKeyService])) - .createInstance(FloatingClickMenu, { - container: this.messageContainer, - menuId: MenuId.TestMessageContent, - getActionArg: () => (subject as MessageSubject).context, - }) - ); + const instaService = this.currentSubjectStore.add(this.instantiationService + .createChild(new ServiceCollection([IContextKeyService, this.messageContextKeyService]))); + + this.currentSubjectStore.add(instaService.createInstance(FloatingClickMenu, { + container: this.messageContainer, + menuId: MenuId.TestMessageContent, + getActionArg: () => (subject as MessageSubject).context, + })); } public onLayoutBody(height: number, width: number) { @@ -1086,7 +1090,7 @@ class TestResultsPeek extends PeekViewWidget { if (!this.scopedContextKeyService) { this.scopedContextKeyService = this._disposables.add(this.contextKeyService.createScoped(container)); TestingContextKeys.isInPeek.bindTo(this.scopedContextKeyService).set(true); - const instaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + const instaService = this._disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); this.content = this._disposables.add(instaService.createInstance(TestResultsViewContent, this.editor, { historyVisible: this.testingPeek.historyVisible, showRevealLocationOnMessages: false, locationForProgress: Testing.ResultsViewId })); } @@ -1098,10 +1102,9 @@ class TestResultsPeek extends PeekViewWidget { super._fillHead(container); const actions: IAction[] = []; - const menu = this.menuService.createMenu(MenuId.TestPeekTitle, this.contextKeyService); - createAndFillInActionBarActions(menu, undefined, actions); + const menu = this.menuService.getMenuActions(MenuId.TestPeekTitle, this.contextKeyService); + createAndFillInActionBarActions(menu, actions); this._actionbarWidget!.push(actions, { label: false, icon: true, index: 0 }); - menu.dispose(); } protected override _fillBody(containerElement: HTMLElement): void { @@ -1377,7 +1380,7 @@ class ScrollableMarkdownMessage extends Disposable { container.appendChild(this.scrollable.getDomNode()); this._register(toDisposable(() => { - container.removeChild(this.scrollable.getDomNode()); + this.scrollable.getDomNode().remove(); })); this.scrollable.scanDomNode(); @@ -2057,6 +2060,7 @@ class OutputPeekTree extends Disposable { result = Iterable.concat( Iterable.single>({ element: new CoverageElement(results, task, coverageService), + collapsible: true, incompressible: true, }), result, @@ -2082,6 +2086,7 @@ class OutputPeekTree extends Disposable { return ({ element: taskElem, incompressible: false, + collapsible: true, children: getTaskChildren(taskElem), }); }); @@ -2092,6 +2097,7 @@ class OutputPeekTree extends Disposable { return { element, incompressible: true, + collapsible: true, collapsed: this.tree.hasElement(element) ? this.tree.isCollapsed(element) : true, children: getResultChildren(result) }; @@ -2551,13 +2557,9 @@ class TreeActionsProvider { const contextOverlay = this.contextKeyService.createOverlay(contextKeys); const result = { primary, secondary }; - const menu = this.menuService.createMenu(id, contextOverlay); - try { - createAndFillInActionBarActions(menu, { arg: element.context }, result, 'inline'); - return result; - } finally { - menu.dispose(); - } + const menu = this.menuService.getMenuActions(id, contextOverlay, { arg: element.context }); + createAndFillInActionBarActions(menu, result, 'inline'); + return result; } } diff --git a/src/vs/workbench/contrib/testing/browser/theme.ts b/src/vs/workbench/contrib/testing/browser/theme.ts index 536c03da5f9..0c088a787c3 100644 --- a/src/vs/workbench/contrib/testing/browser/theme.ts +++ b/src/vs/workbench/contrib/testing/browser/theme.ts @@ -30,33 +30,13 @@ export const testingColorIconPassed = registerColor('testing.iconPassed', { hcLight: '#007100' }, localize('testing.iconPassed', "Color for the 'passed' icon in the test explorer.")); -export const testingColorRunAction = registerColor('testing.runAction', { - dark: testingColorIconPassed, - light: testingColorIconPassed, - hcDark: testingColorIconPassed, - hcLight: testingColorIconPassed -}, localize('testing.runAction', "Color for 'run' icons in the editor.")); +export const testingColorRunAction = registerColor('testing.runAction', testingColorIconPassed, localize('testing.runAction', "Color for 'run' icons in the editor.")); -export const testingColorIconQueued = registerColor('testing.iconQueued', { - dark: '#cca700', - light: '#cca700', - hcDark: '#cca700', - hcLight: '#cca700' -}, localize('testing.iconQueued', "Color for the 'Queued' icon in the test explorer.")); +export const testingColorIconQueued = registerColor('testing.iconQueued', '#cca700', localize('testing.iconQueued', "Color for the 'Queued' icon in the test explorer.")); -export const testingColorIconUnset = registerColor('testing.iconUnset', { - dark: '#848484', - light: '#848484', - hcDark: '#848484', - hcLight: '#848484' -}, localize('testing.iconUnset', "Color for the 'Unset' icon in the test explorer.")); +export const testingColorIconUnset = registerColor('testing.iconUnset', '#848484', localize('testing.iconUnset', "Color for the 'Unset' icon in the test explorer.")); -export const testingColorIconSkipped = registerColor('testing.iconSkipped', { - dark: '#848484', - light: '#848484', - hcDark: '#848484', - hcLight: '#848484' -}, localize('testing.iconSkipped', "Color for the 'Skipped' icon in the test explorer.")); +export const testingColorIconSkipped = registerColor('testing.iconSkipped', '#848484', localize('testing.iconSkipped', "Color for the 'Skipped' icon in the test explorer.")); export const testingPeekBorder = registerColor('testing.peekBorder', { dark: editorErrorForeground, @@ -135,19 +115,9 @@ export const testingUncoveredGutterBackground = registerColor('testing.uncovered hcLight: chartsRed }, localize('testing.uncoveredGutterBackground', 'Gutter color of regions where code not covered.')); -export const testingCoverCountBadgeBackground = registerColor('testing.coverCountBadgeBackground', { - dark: badgeBackground, - light: badgeBackground, - hcDark: badgeBackground, - hcLight: badgeBackground -}, localize('testing.coverCountBadgeBackground', 'Background for the badge indicating execution count')); +export const testingCoverCountBadgeBackground = registerColor('testing.coverCountBadgeBackground', badgeBackground, localize('testing.coverCountBadgeBackground', 'Background for the badge indicating execution count')); -export const testingCoverCountBadgeForeground = registerColor('testing.coverCountBadgeForeground', { - dark: badgeForeground, - light: badgeForeground, - hcDark: badgeForeground, - hcLight: badgeForeground -}, localize('testing.coverCountBadgeForeground', 'Foreground for the badge indicating execution count')); +export const testingCoverCountBadgeForeground = registerColor('testing.coverCountBadgeForeground', badgeForeground, localize('testing.coverCountBadgeForeground', 'Foreground for the badge indicating execution count')); export const testMessageSeverityColors: { [K in TestMessageType]: { @@ -170,12 +140,12 @@ export const testMessageSeverityColors: { [TestMessageType.Output]: { decorationForeground: registerColor( 'testing.message.info.decorationForeground', - { dark: transparent(editorForeground, 0.5), light: transparent(editorForeground, 0.5), hcDark: transparent(editorForeground, 0.5), hcLight: transparent(editorForeground, 0.5) }, + transparent(editorForeground, 0.5), localize('testing.message.info.decorationForeground', 'Text color of test info messages shown inline in the editor.') ), marginBackground: registerColor( 'testing.message.info.lineBackground', - { dark: null, light: null, hcDark: null, hcLight: null }, + null, localize('testing.message.info.marginBackground', 'Margin color beside info messages shown inline in the editor.') ), }, @@ -190,47 +160,17 @@ export const testStatesToIconColors: { [K in TestResultState]?: string } = { [TestResultState.Skipped]: testingColorIconSkipped, }; -export const testingRetiredColorIconErrored = registerColor('testing.iconErrored.retired', { - dark: transparent(testingColorIconErrored, 0.7), - light: transparent(testingColorIconErrored, 0.7), - hcDark: transparent(testingColorIconErrored, 0.7), - hcLight: transparent(testingColorIconErrored, 0.7) -}, localize('testing.iconErrored.retired', "Retired color for the 'Errored' icon in the test explorer.")); +export const testingRetiredColorIconErrored = registerColor('testing.iconErrored.retired', transparent(testingColorIconErrored, 0.7), localize('testing.iconErrored.retired', "Retired color for the 'Errored' icon in the test explorer.")); -export const testingRetiredColorIconFailed = registerColor('testing.iconFailed.retired', { - dark: transparent(testingColorIconFailed, 0.7), - light: transparent(testingColorIconFailed, 0.7), - hcDark: transparent(testingColorIconFailed, 0.7), - hcLight: transparent(testingColorIconFailed, 0.7) -}, localize('testing.iconFailed.retired', "Retired color for the 'failed' icon in the test explorer.")); +export const testingRetiredColorIconFailed = registerColor('testing.iconFailed.retired', transparent(testingColorIconFailed, 0.7), localize('testing.iconFailed.retired', "Retired color for the 'failed' icon in the test explorer.")); -export const testingRetiredColorIconPassed = registerColor('testing.iconPassed.retired', { - dark: transparent(testingColorIconPassed, 0.7), - light: transparent(testingColorIconPassed, 0.7), - hcDark: transparent(testingColorIconPassed, 0.7), - hcLight: transparent(testingColorIconPassed, 0.7) -}, localize('testing.iconPassed.retired', "Retired color for the 'passed' icon in the test explorer.")); +export const testingRetiredColorIconPassed = registerColor('testing.iconPassed.retired', transparent(testingColorIconPassed, 0.7), localize('testing.iconPassed.retired', "Retired color for the 'passed' icon in the test explorer.")); -export const testingRetiredColorIconQueued = registerColor('testing.iconQueued.retired', { - dark: transparent(testingColorIconQueued, 0.7), - light: transparent(testingColorIconQueued, 0.7), - hcDark: transparent(testingColorIconQueued, 0.7), - hcLight: transparent(testingColorIconQueued, 0.7) -}, localize('testing.iconQueued.retired', "Retired color for the 'Queued' icon in the test explorer.")); +export const testingRetiredColorIconQueued = registerColor('testing.iconQueued.retired', transparent(testingColorIconQueued, 0.7), localize('testing.iconQueued.retired', "Retired color for the 'Queued' icon in the test explorer.")); -export const testingRetiredColorIconUnset = registerColor('testing.iconUnset.retired', { - dark: transparent(testingColorIconUnset, 0.7), - light: transparent(testingColorIconUnset, 0.7), - hcDark: transparent(testingColorIconUnset, 0.7), - hcLight: transparent(testingColorIconUnset, 0.7) -}, localize('testing.iconUnset.retired', "Retired color for the 'Unset' icon in the test explorer.")); +export const testingRetiredColorIconUnset = registerColor('testing.iconUnset.retired', transparent(testingColorIconUnset, 0.7), localize('testing.iconUnset.retired', "Retired color for the 'Unset' icon in the test explorer.")); -export const testingRetiredColorIconSkipped = registerColor('testing.iconSkipped.retired', { - dark: transparent(testingColorIconSkipped, 0.7), - light: transparent(testingColorIconSkipped, 0.7), - hcDark: transparent(testingColorIconSkipped, 0.7), - hcLight: transparent(testingColorIconSkipped, 0.7) -}, localize('testing.iconSkipped.retired', "Retired color for the 'Skipped' icon in the test explorer.")); +export const testingRetiredColorIconSkipped = registerColor('testing.iconSkipped.retired', transparent(testingColorIconSkipped, 0.7), localize('testing.iconSkipped.retired', "Retired color for the 'Skipped' icon in the test explorer.")); export const testStatesToRetiredIconColors: { [K in TestResultState]?: string } = { [TestResultState.Errored]: testingRetiredColorIconErrored, diff --git a/src/vs/workbench/contrib/testing/common/configuration.ts b/src/vs/workbench/contrib/testing/common/configuration.ts index ec9e50d67f0..91ca1a2bd18 100644 --- a/src/vs/workbench/contrib/testing/common/configuration.ts +++ b/src/vs/workbench/contrib/testing/common/configuration.ts @@ -159,7 +159,7 @@ export const testingConfiguration: IConfigurationNode = { description: localize('testing.openTesting', "Controls when the testing view should open.") }, [TestingConfigKeys.AlwaysRevealTestOnStateChange]: { - markdownDescription: localize('testing.alwaysRevealTestOnStateChange', "Always reveal the executed test when `#testing.followRunningTest#` is on. If this setting is turned off, only failed tests will be revealed."), + markdownDescription: localize('testing.alwaysRevealTestOnStateChange', "Always reveal the executed test when {0} is on. If this setting is turned off, only failed tests will be revealed.", '`#testing.followRunningTest#`'), type: 'boolean', default: false, }, diff --git a/src/vs/workbench/contrib/testing/common/testCoverage.ts b/src/vs/workbench/contrib/testing/common/testCoverage.ts index 321434bd602..aae8f0af362 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverage.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverage.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assert } from 'vs/base/common/assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ResourceMap } from 'vs/base/common/map'; import { deepClone } from 'vs/base/common/objects'; @@ -13,10 +12,10 @@ import { URI } from 'vs/base/common/uri'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; -import { CoverageDetails, ICoverageCount, IFileCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; +import { CoverageDetails, DetailType, ICoverageCount, IFileCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; export interface ICoverageAccessor { - getCoverageDetails: (id: string, token: CancellationToken) => Promise; + getCoverageDetails: (id: string, testId: string | undefined, token: CancellationToken) => Promise; } let incId = 0; @@ -30,9 +29,6 @@ export class TestCoverage { public readonly tree = new WellDefinedPrefixTree(); public readonly associatedData = new Map(); - /** Test IDs that have per-test coverage in this output. */ - public readonly perTestCoverageIDs = new Set(); - constructor( public readonly result: LiveTestResult, public readonly fromTaskId: string, @@ -40,6 +36,21 @@ export class TestCoverage { private readonly accessor: ICoverageAccessor, ) { } + /** Gets all test IDs that were included in this test run. */ + public *allPerTestIDs() { + const seen = new Set(); + for (const root of this.tree.nodes) { + if (root.value && root.value.perTestData) { + for (const id of root.value.perTestData) { + if (!seen.has(id)) { + seen.add(id); + yield id; + } + } + } + } + } + public append(coverage: IFileCoverage, tx: ITransaction | undefined) { const previous = this.getComputedForUri(coverage.uri); const result = this.result; @@ -59,24 +70,13 @@ export class TestCoverage { // version. const canonical = [...this.treePathForUri(coverage.uri, /* canonical = */ true)]; const chain: IPrefixTreeNode[] = []; - const isPerTestCoverage = !!coverage.testId; - if (coverage.testId) { - this.perTestCoverageIDs.add(coverage.testId.toString()); - } + this.tree.mutatePath(this.treePathForUri(coverage.uri, /* canonical = */ false), node => { chain.push(node); if (chain.length === canonical.length) { // we reached our destination node, apply the coverage as necessary: - if (isPerTestCoverage) { - const v = node.value ??= new FileCoverage(IFileCoverage.empty(String(incId++), coverage.uri), result, this.accessor); - assert(v instanceof FileCoverage, 'coverage is unexpectedly computed'); - v.perTestData ??= new Map(); - const perTest = new FileCoverage(coverage, result, this.accessor); - perTest.isForTest = { id: coverage.testId!, parent: v }; - v.perTestData.set(coverage.testId!.toString(), perTest); - this.fileCoverage.set(coverage.uri, v); - } else if (node.value) { + if (node.value) { const v = node.value; // if ID was generated from a test-specific coverage, reassign it to get its real ID in the extension host. v.id = coverage.id; @@ -87,7 +87,7 @@ export class TestCoverage { const v = node.value = new FileCoverage(coverage, result, this.accessor); this.fileCoverage.set(coverage.uri, v); } - } else if (!isPerTestCoverage) { + } else { // Otherwise, if this is not a partial per-test coverage, merge the // coverage changes into the chain. Per-test coverages are not complete // and we don't want to consider them for computation. @@ -104,9 +104,16 @@ export class TestCoverage { node.value.didChange.trigger(tx); } } + + if (coverage.testIds) { + node.value!.perTestData ??= new Set(); + for (const id of coverage.testIds) { + node.value!.perTestData.add(id); + } + } }); - if (chain && !isPerTestCoverage) { + if (chain) { this.didAddCoverage.trigger(tx, chain); } } @@ -118,21 +125,15 @@ export class TestCoverage { const tree = new WellDefinedPrefixTree(); for (const node of this.tree.values()) { if (node instanceof FileCoverage) { - const fileData = node.perTestData?.get(testId.toString()); - if (!fileData) { + if (!node.perTestData?.has(testId.toString())) { continue; } - const canonical = [...this.treePathForUri(fileData.uri, /* canonical = */ true)]; + const canonical = [...this.treePathForUri(node.uri, /* canonical = */ true)]; const chain: IPrefixTreeNode[] = []; - tree.mutatePath(this.treePathForUri(fileData.uri, /* canonical = */ false), node => { - chain.push(node); - - if (chain.length === canonical.length) { - node.value = fileData; - } else { - node.value ??= new BypassedFileCoverage(this.treePathToUri(canonical.slice(0, chain.length)), fileData.fromResult); - } + tree.mutatePath(this.treePathForUri(node.uri, /* canonical = */ false), n => { + chain.push(n); + n.value ??= new BypassedFileCoverage(this.treePathToUri(canonical.slice(0, chain.length)), node.fromResult); }); } } @@ -208,6 +209,11 @@ export abstract class AbstractFileCoverage { return getTotalCoveragePercent(this.statement, this.branch, this.declaration); } + /** + * Per-test coverage data for this file, if available. + */ + public perTestData?: Set; + constructor(coverage: IFileCoverage, public readonly fromResult: LiveTestResult) { this.id = coverage.id; this.uri = coverage.uri; @@ -235,31 +241,46 @@ export class BypassedFileCoverage extends ComputedFileCoverage { export class FileCoverage extends AbstractFileCoverage { private _details?: Promise; private resolved?: boolean; + private _detailsForTest?: Map>; /** Gets whether details are synchronously available */ public get hasSynchronousDetails() { return this._details instanceof Array || this.resolved; } - /** - * Per-test coverage data for this file, if available. - */ - public perTestData?: Map; - - /** - * If this is for a single test item, gets the test item. - */ - public isForTest?: { id: TestId; parent: FileCoverage }; - constructor(coverage: IFileCoverage, fromResult: LiveTestResult, private readonly accessor: ICoverageAccessor) { super(coverage, fromResult); } + /** + * Gets per-line coverage details. + */ + public async detailsForTest(_testId: TestId, token = CancellationToken.None) { + this._detailsForTest ??= new Map(); + const testId = _testId.toString(); + const prev = this._detailsForTest.get(testId); + if (prev) { + return prev; + } + + const promise = (async () => { + try { + return await this.accessor.getCoverageDetails(this.id, testId, token); + } catch (e) { + this._detailsForTest?.delete(testId); + throw e; + } + })(); + + this._detailsForTest.set(testId, promise); + return promise; + } + /** * Gets per-line coverage details. */ public async details(token = CancellationToken.None) { - this._details ??= this.accessor.getCoverageDetails(this.id, token); + this._details ??= this.accessor.getCoverageDetails(this.id, undefined, token); try { const d = await this._details; @@ -271,3 +292,30 @@ export class FileCoverage extends AbstractFileCoverage { } } } + +export const totalFromCoverageDetails = (uri: URI, details: CoverageDetails[]): IFileCoverage => { + const fc: IFileCoverage = { + id: '', + uri, + statement: ICoverageCount.empty(), + }; + + for (const detail of details) { + if (detail.type === DetailType.Statement) { + fc.statement.total++; + fc.statement.total += detail.count ? 1 : 0; + + for (const branch of detail.branches || []) { + fc.branch ??= ICoverageCount.empty(); + fc.branch.total++; + fc.branch.covered += branch.count ? 1 : 0; + } + } else { + fc.declaration ??= ICoverageCount.empty(); + fc.declaration.total++; + fc.declaration.covered += detail.count ? 1 : 0; + } + } + + return fc; +}; diff --git a/src/vs/workbench/contrib/testing/common/testCoverageService.ts b/src/vs/workbench/contrib/testing/common/testCoverageService.ts index e1b62d541dc..4433a713d2c 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverageService.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverageService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Iterable } from 'vs/base/common/iterator'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IObservable, ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -35,6 +36,11 @@ export interface ITestCoverageService { */ readonly filterToTest: ISettableObservable; + /** + * Whether inline coverage is shown. + */ + readonly showInline: ISettableObservable; + /** * Opens a test coverage report from a task, optionally focusing it in the editor. */ @@ -52,6 +58,7 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ public readonly selected = observableValue('testCoverage', undefined); public readonly filterToTest = observableValue('filterToTest', undefined); + public readonly showInline = observableValue('inlineCoverage', false); constructor( @IContextKeyService contextKeyService: IContextKeyService, @@ -68,6 +75,12 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ reader => toolbarConfig.read(reader), )); + this._register(bindContextKey( + TestingContextKeys.inlineCoverageEnabled, + contextKeyService, + reader => this.showInline.read(reader), + )); + this._register(bindContextKey( TestingContextKeys.isTestCoverageOpen, contextKeyService, @@ -77,7 +90,7 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ this._register(bindContextKey( TestingContextKeys.hasPerTestCoverage, contextKeyService, - reader => !!this.selected.read(reader)?.perTestCoverageIDs.size, + reader => !Iterable.isEmpty(this.selected.read(reader)?.allPerTestIDs()), )); this._register(bindContextKey( diff --git a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts index 3ab130d114f..e9c1275a79c 100644 --- a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts +++ b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts @@ -199,6 +199,7 @@ export const enum TestFilterTerm { Failed = '@failed', Executed = '@executed', CurrentDoc = '@doc', + OpenedFiles = '@openedFiles', Hidden = '@hidden', } @@ -206,5 +207,6 @@ const allTestFilterTerms: readonly TestFilterTerm[] = [ TestFilterTerm.Failed, TestFilterTerm.Executed, TestFilterTerm.CurrentDoc, + TestFilterTerm.OpenedFiles, TestFilterTerm.Hidden, ]; diff --git a/src/vs/workbench/contrib/testing/common/testId.ts b/src/vs/workbench/contrib/testing/common/testId.ts index 79fcec77b1d..0b66669342a 100644 --- a/src/vs/workbench/contrib/testing/common/testId.ts +++ b/src/vs/workbench/contrib/testing/common/testId.ts @@ -105,7 +105,7 @@ export class TestId { * todo@connor4312: review usages of this to see if using the WellDefinedPrefixTree is better */ public static isChild(maybeParent: string, maybeChild: string) { - return maybeChild.startsWith(maybeParent) && maybeChild[maybeParent.length] === TestIdPathParts.Delimiter; + return maybeChild[maybeParent.length] === TestIdPathParts.Delimiter && maybeChild.startsWith(maybeParent); } /** diff --git a/src/vs/workbench/contrib/testing/common/testProfileService.ts b/src/vs/workbench/contrib/testing/common/testProfileService.ts index adf73855e6c..d01101c6919 100644 --- a/src/vs/workbench/contrib/testing/common/testProfileService.ts +++ b/src/vs/workbench/contrib/testing/common/testProfileService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; import { Disposable } from 'vs/base/common/lifecycle'; import { deepClone } from 'vs/base/common/objects'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -64,7 +65,7 @@ export interface ITestProfileService { /** * Gets the default profiles to be run for a given run group. */ - getGroupDefaultProfiles(group: TestRunProfileBitset): ITestRunProfile[]; + getGroupDefaultProfiles(group: TestRunProfileBitset, controllerId?: string): ITestRunProfile[]; /** * Sets the default profiles to be run for a given run group. @@ -252,20 +253,17 @@ export class TestProfileService extends Disposable implements ITestProfileServic } /** @inheritdoc */ - public getGroupDefaultProfiles(group: TestRunProfileBitset) { - let defaults: ITestRunProfile[] = []; - for (const { profiles } of this.controllerProfiles.values()) { - defaults = defaults.concat(profiles.filter(c => c.group === group && c.isDefault)); - } + public getGroupDefaultProfiles(group: TestRunProfileBitset, controllerId?: string) { + const allProfiles = controllerId + ? (this.controllerProfiles.get(controllerId)?.profiles || []) + : [...Iterable.flatMap(this.controllerProfiles.values(), c => c.profiles)]; + const defaults = allProfiles.filter(c => c.group === group && c.isDefault); // have *some* default profile to run if none are set otherwise if (defaults.length === 0) { - for (const { profiles } of this.controllerProfiles.values()) { - const first = profiles.find(p => p.group === group); - if (first) { - defaults.push(first); - break; - } + const first = allProfiles.find(p => p.group === group); + if (first) { + defaults.push(first); } } diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 3035be654df..88bdd37b5c2 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -16,7 +16,7 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITestResult, LiveTestResult, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { ExtensionRunTestsRequest, ITestRunProfile, ResolvedTestRunRequest, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; +import { ExtensionRunTestsRequest, ITestRunProfile, ResolvedTestRunRequest, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; export type ResultChangeEvent = | { completed: LiveTestResult } @@ -153,11 +153,11 @@ export class TestResultService extends Disposable implements ITestResultService targets: [], exclude: req.exclude, continuous: req.continuous, + group: profile?.group ?? TestRunProfileBitset.Run, }; if (profile) { resolved.targets.push({ - profileGroup: profile.group, profileId: profile.profileId, controllerId: req.controllerId, testIds: req.include, diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index d3026db22eb..48ebe0e230e 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -3,19 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { assert } from 'vs/base/common/assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { IPrefixTreeNode, WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { AbstractIncrementalTestCollection, ICallProfileRunHandler, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ResolvedTestRunRequest, IStartControllerTests, IStartControllerTestsResult, TestItemExpandState, TestRunProfileBitset, TestsDiff, TestMessageFollowupResponse, TestMessageFollowupRequest } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusions'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; +import { AbstractIncrementalTestCollection, ICallProfileRunHandler, IncrementalTestCollectionItem, InternalTestItem, IStartControllerTests, IStartControllerTestsResult, ITestItemContext, ResolvedTestRunRequest, TestItemExpandState, TestMessageFollowupRequest, TestMessageFollowupResponse, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; export const ITestService = createDecorator('testService'); @@ -201,12 +203,9 @@ export const testsUnderUri = async function* (testService: ITestService, ident: // tests already encompass their children. if (!test) { // no-op - } else if (!test.item.uri) { - queue.push(test.children.values()); - continue; - } else if (ident.extUri.isEqualOrParent(test.item.uri, uri)) { + } else if (test.item.uri && ident.extUri.isEqualOrParent(test.item.uri, uri)) { yield test; - } else if (ident.extUri.isEqualOrParent(uri, test.item.uri)) { + } else if (!test.item.uri || ident.extUri.isEqualOrParent(uri, test.item.uri)) { if (test.expand === TestItemExpandState.Expandable) { await testService.collection.expand(test.item.extId, 1); } @@ -219,6 +218,64 @@ export const testsUnderUri = async function* (testService: ITestService, ident: } }; +/** + * Simplifies the array of tests by preferring test item parents if all of + * their children are included. + */ +export const simplifyTestsToExecute = (collection: IMainThreadTestCollection, tests: IncrementalTestCollectionItem[]): IncrementalTestCollectionItem[] => { + if (tests.length < 2) { + return tests; + } + + const tree = new WellDefinedPrefixTree(); + for (const test of tests) { + tree.insert(TestId.fromString(test.item.extId).path, test); + } + + const out: IncrementalTestCollectionItem[] = []; + + // Returns the node if it and any children should be included. Otherwise + // pushes into the `out` any individual children that should be included. + const process = (currentId: string[], node: IPrefixTreeNode) => { + // directly included, don't try to over-specify, and children should be ignored + if (node.value) { + return node.value; + } + + assert(!!node.children, 'expect to have children'); + + const thisChildren: IncrementalTestCollectionItem[] = []; + for (const [part, child] of node.children) { + currentId.push(part); + const c = process(currentId, child); + if (c) { thisChildren.push(c); } + currentId.pop(); + } + + if (!thisChildren.length) { + return; + } + + // If there are multiple children and we have all of them, then tell the + // parent this node should be included. Otherwise include children individually. + const id = new TestId(currentId); + const test = collection.getNodeById(id.toString()); + if (test?.children.size === thisChildren.length) { + return test; + } + + out.push(...thisChildren); + return; + }; + + for (const [id, node] of tree.entries) { + const n = process([id], node); + if (n) { out.push(n); } + } + + return out; +}; + /** * A run request that expresses the intent of the request and allows the * test service to resolve the specifics of the group. diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index fd3ffcd0999..7b0e47cd891 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -28,7 +28,7 @@ import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { AmbiguousRunTestsRequest, IMainThreadTestController, IMainThreadTestHostProxy, ITestFollowups, ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { ResolvedTestRunRequest, TestDiffOpType, TestMessageFollowupRequest, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; +import { InternalTestItem, ITestRunProfile, ResolvedTestRunRequest, TestDiffOpType, TestMessageFollowupRequest, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class TestService extends Disposable implements ITestService { @@ -133,25 +133,37 @@ export class TestService extends Disposable implements ITestService { * @inheritdoc */ public async runTests(req: AmbiguousRunTestsRequest, token = CancellationToken.None): Promise { + // We try to ensure that all tests in the request will be run, preferring + // to use default profiles for each controller when possible. + const byProfile: { profile: ITestRunProfile; tests: InternalTestItem[] }[] = []; + for (const test of req.tests) { + const existing = byProfile.find(p => canUseProfileWithTest(p.profile, test)); + if (existing) { + existing.tests.push(test); + continue; + } + + const allProfiles = this.testProfiles.getControllerProfiles(test.controllerId) + .filter(p => (p.group & req.group) !== 0 && canUseProfileWithTest(p, test)); + const bestProfile = allProfiles.find(p => p.isDefault) || allProfiles[0]; + if (!bestProfile) { + continue; + } + + byProfile.push({ profile: bestProfile, tests: [test] }); + } + const resolved: ResolvedTestRunRequest = { - targets: [], + targets: byProfile.map(({ profile, tests }) => ({ + profileId: profile.profileId, + controllerId: tests[0].controllerId, + testIds: tests.map(t => t.item.extId), + })), + group: req.group, exclude: req.exclude?.map(t => t.item.extId), continuous: req.continuous, }; - // First, try to run the tests using the default run profiles... - for (const profile of this.testProfiles.getGroupDefaultProfiles(req.group)) { - const testIds = req.tests.filter(t => canUseProfileWithTest(profile, t)).map(t => t.item.extId); - if (testIds.length) { - resolved.targets.push({ - testIds: testIds, - profileGroup: profile.group, - profileId: profile.profileId, - controllerId: profile.controllerId, - }); - } - } - // If no tests are covered by the defaults, just use whatever the defaults // for their controller are. This can happen if the user chose specific // profiles for the run button, but then asked to run a single test from the @@ -169,7 +181,6 @@ export class TestService extends Disposable implements ITestService { if (profile) { resolved.targets.push({ testIds: byProfile.map(t => t.test.item.extId), - profileGroup: req.group, profileId: profile.profileId, controllerId: profile.controllerId, }); diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 75f7b371362..5a90948fc68 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -78,10 +78,10 @@ export interface ITestRunProfile { * and extension host. */ export interface ResolvedTestRunRequest { + group: TestRunProfileBitset; targets: { testIds: string[]; controllerId: string; - profileGroup: TestRunProfileBitset; profileId: number; }[]; exclude?: string[]; @@ -573,7 +573,7 @@ export namespace ICoverageCount { export interface IFileCoverage { id: string; uri: URI; - testId?: TestId; + testIds?: string[]; statement: ICoverageCount; branch?: ICoverageCount; declaration?: ICoverageCount; @@ -583,7 +583,7 @@ export namespace IFileCoverage { export interface Serialized { id: string; uri: UriComponents; - testId: string | undefined; + testIds: string[] | undefined; statement: ICoverageCount; branch?: ICoverageCount; declaration?: ICoverageCount; @@ -594,7 +594,7 @@ export namespace IFileCoverage { statement: original.statement, branch: original.branch, declaration: original.declaration, - testId: original.testId?.toString(), + testIds: original.testIds, uri: original.uri.toJSON(), }); @@ -603,14 +603,13 @@ export namespace IFileCoverage { statement: serialized.statement, branch: serialized.branch, declaration: serialized.declaration, - testId: serialized.testId ? TestId.fromString(serialized.testId) : undefined, + testIds: serialized.testIds, uri: uriIdentity.asCanonicalUri(URI.revive(serialized.uri)), }); export const empty = (id: string, uri: URI): IFileCoverage => ({ id, uri, - testId: undefined, statement: ICoverageCount.empty(), }); } diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 96682ebf7c9..2078a1d0efd 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -25,6 +25,7 @@ export namespace TestingContextKeys { export const hasPerTestCoverage = new RawContextKey('testing.hasPerTestCoverage', false, { type: 'boolean', description: localize('testing.hasPerTestCoverage', 'Indicates whether per-test coverage is available') }); export const isCoverageFilteredToTest = new RawContextKey('testing.isCoverageFilteredToTest', false, { type: 'boolean', description: localize('testing.isCoverageFilteredToTest', 'Indicates whether coverage has been filterd to a single test') }); export const coverageToolbarEnabled = new RawContextKey('testing.coverageToolbarEnabled', true, { type: 'boolean', description: localize('testing.coverageToolbarEnabled', 'Indicates whether the coverage toolbar is enabled') }); + export const inlineCoverageEnabled = new RawContextKey('testing.inlineCoverageEnabled', false, { type: 'boolean', description: localize('testing.inlineCoverageEnabled', 'Indicates whether inline coverage is shown') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { [TestRunProfileBitset.Run]: hasRunnableTests, @@ -74,4 +75,8 @@ export namespace TestingContextKeys { type: 'string', description: localize('testing.testResultState', 'Value available testing/item/result indicating the state of the item.') }); + export const testProfileContextGroup = new RawContextKey('testing.profile.context.group', undefined, { + type: 'string', + description: localize('testing.profile.context.group', 'Type of menu where the configure testing profile submenu exists. Either "run", "debug", or "coverage"') + }); } diff --git a/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts b/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts index b8053615c6b..47be6dfacba 100644 --- a/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts +++ b/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts @@ -177,10 +177,10 @@ export class TestingContinuousRunService extends Disposable implements ITestingC if (actualProfiles.length) { this.testService.startContinuousRun({ continuous: true, + group: actualProfiles[0].group, targets: actualProfiles.map(p => ({ testIds: [testId ?? p.controllerId], controllerId: p.controllerId, - profileGroup: p.group, profileId: p.profileId })), }, cts.token); diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts index ea3ab5dfe0f..f0709ccc4e5 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/nameProjection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ListProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/listProjection'; @@ -92,4 +92,3 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { ]); }); }); - diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts index cebf9e01a5d..d59d15ef1f8 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/treeProjection.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -267,5 +267,52 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { ]); }); -}); + test('fixes #213316 (single root)', async () => { + harness.flush(); + assert.deepStrictEqual(harness.tree.getRendered(), [ + { e: 'a' }, { e: 'b' } + ]); + harness.pushDiff({ + op: TestDiffOpType.Remove, + itemId: new TestId(['ctrlId', 'id-a']).toString(), + }); + harness.flush(); + assert.deepStrictEqual(harness.tree.getRendered(), [ + { e: 'b' } + ]); + }); + test('fixes #213316 (multi root)', async () => { + harness.pushDiff({ + op: TestDiffOpType.Add, + item: { controllerId: 'ctrl2', expand: TestItemExpandState.Expanded, item: new TestTestItem(new TestId(['ctrlId2']), 'c').toTestItem() }, + }, { + op: TestDiffOpType.Add, + item: { controllerId: 'ctrl2', expand: TestItemExpandState.NotExpandable, item: new TestTestItem(new TestId(['ctrlId2', 'id-c']), 'ca').toTestItem() }, + }); + harness.flush(); + assert.deepStrictEqual(harness.flush(), [ + { e: 'c', children: [{ e: 'ca' }] }, + { e: 'root', children: [{ e: 'a' }, { e: 'b' }] } + ]); + + harness.pushDiff({ + op: TestDiffOpType.Remove, + itemId: new TestId(['ctrlId', 'id-a']).toString(), + }); + harness.flush(); + assert.deepStrictEqual(harness.tree.getRendered(), [ + { e: 'c', children: [{ e: 'ca' }] }, + { e: 'root', children: [{ e: 'b' }] } + ]); + + harness.pushDiff({ + op: TestDiffOpType.Remove, + itemId: new TestId(['ctrlId', 'id-b']).toString(), + }); + harness.flush(); + assert.deepStrictEqual(harness.tree.getRendered(), [ + { e: 'ca' } + ]); + }); +}); diff --git a/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts b/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts index ea8bb1e5b6e..9ab4c553649 100644 --- a/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts @@ -8,14 +8,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { SinonSandbox, createSandbox } from 'sinon'; -import { Iterable } from 'vs/base/common/iterator'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { onObservableChange } from 'vs/workbench/contrib/testing/common/observableUtils'; import { ICoverageAccessor, TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; -import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { IFileCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; @@ -133,58 +131,4 @@ suite('TestCoverage', () => { ], ]); }); - - test('adds per-test data to files', async () => { - const { raw1 } = addTests(); - - const raw3: IFileCoverage = { - id: '1', - testId: TestId.fromString('my-test'), - uri: URI.file('/path/to/file'), - statement: { covered: 12, total: 24 }, - branch: { covered: 7, total: 10 }, - declaration: { covered: 2, total: 5 }, - }; - testCoverage.append(raw3, undefined); - - const fileCoverage = testCoverage.getUri(raw1.uri); - assert.strictEqual(fileCoverage?.perTestData?.size, 1); - - const perTestCoverage = Iterable.first(fileCoverage!.perTestData!.values()); - assert.deepStrictEqual(perTestCoverage?.statement, raw3.statement); - assert.deepStrictEqual(perTestCoverage?.branch, raw3.branch); - assert.deepStrictEqual(perTestCoverage?.declaration, raw3.declaration); - - // should be unchanged: - assert.deepEqual(fileCoverage?.statement, { covered: 10, total: 20 }); - const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); - assert.deepEqual(dirCoverage?.statement, { covered: 15, total: 30 }); - }); - - test('works if per-test data is added first', async () => { - const raw3: IFileCoverage = { - id: '1', - testId: TestId.fromString('my-test'), - uri: URI.file('/path/to/file'), - statement: { covered: 12, total: 24 }, - branch: { covered: 7, total: 10 }, - declaration: { covered: 2, total: 5 }, - }; - testCoverage.append(raw3, undefined); - - const fileCoverage = testCoverage.getUri(raw3.uri); - - addTests(); - - assert.strictEqual(fileCoverage?.perTestData?.size, 1); - const perTestCoverage = Iterable.first(fileCoverage!.perTestData!.values()); - assert.deepStrictEqual(perTestCoverage?.statement, raw3.statement); - assert.deepStrictEqual(perTestCoverage?.branch, raw3.branch); - assert.deepStrictEqual(perTestCoverage?.declaration, raw3.declaration); - - // should be the expected values: - assert.deepEqual(fileCoverage?.statement, { covered: 10, total: 20 }); - const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); - assert.deepEqual(dirCoverage?.statement, { covered: 15, total: 30 }); - }); }); diff --git a/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts b/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts index 2be30fb7d2a..192515892c7 100644 --- a/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { InMemoryStorageService } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts b/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts index 1d5771ae4bb..c83d3e7d3b9 100644 --- a/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testProfileService.test.ts @@ -5,7 +5,7 @@ -import * as assert from 'assert'; +import assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; diff --git a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts index da47d907d00..4ccd1f6da36 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -32,8 +32,8 @@ suite('Workbench - Test Results Service', () => { let tests: TestTestCollection; const defaultOpts = (testIds: string[]): ResolvedTestRunRequest => ({ + group: TestRunProfileBitset.Run, targets: [{ - profileGroup: TestRunProfileBitset.Run, profileId: 0, controllerId: 'ctrlId', testIds, diff --git a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts index d040484a930..c7c61884c44 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { range } from 'vs/base/common/arrays'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -12,6 +12,7 @@ import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtil import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { InMemoryResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; +import { TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -23,7 +24,7 @@ suite('Workbench - Test Result Storage', () => { const t = ds.add(new LiveTestResult( '', true, - { targets: [] }, + { targets: [], group: TestRunProfileBitset.Run }, NullTelemetryService, )); diff --git a/src/vs/workbench/contrib/testing/test/common/testService.test.ts b/src/vs/workbench/contrib/testing/test/common/testService.test.ts new file mode 100644 index 00000000000..66ae8c791f3 --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/common/testService.test.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { simplifyTestsToExecute } from 'vs/workbench/contrib/testing/common/testService'; +import { getInitializedMainTestCollection, makeSimpleStubTree } from 'vs/workbench/contrib/testing/test/common/testStubs'; + +suite('Workbench - Test Service', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('simplifyTestsToExecute', () => { + const tree1 = { + a: { + b1: { + c1: { + d: undefined + }, + c2: { + d: undefined + }, + }, + b2: undefined, + } + } as const; + + test('noop on single item', async () => { + const c = await getInitializedMainTestCollection(makeSimpleStubTree(tree1)); + + const t = simplifyTestsToExecute(c, [ + c.getNodeById(new TestId(['ctrlId', 'a', 'b1']).toString())! + ]); + + assert.deepStrictEqual(t.map(t => t.item.extId.toString()), [ + new TestId(['ctrlId', 'a', 'b1']).toString() + ]); + }); + + test('goes to common root 1', async () => { + const c = await getInitializedMainTestCollection(makeSimpleStubTree(tree1)); + + const t = simplifyTestsToExecute(c, [ + c.getNodeById(new TestId(['ctrlId', 'a', 'b1', 'c1', 'd']).toString())!, + c.getNodeById(new TestId(['ctrlId', 'a', 'b1', 'c2']).toString())!, + ]); + + assert.deepStrictEqual(t.map(t => t.item.extId.toString()), [ + new TestId(['ctrlId', 'a', 'b1']).toString() + ]); + }); + + test('goes to common root 2', async () => { + const c = await getInitializedMainTestCollection(makeSimpleStubTree(tree1)); + + const t = simplifyTestsToExecute(c, [ + c.getNodeById(new TestId(['ctrlId', 'a', 'b1', 'c1']).toString())!, + c.getNodeById(new TestId(['ctrlId', 'a', 'b1']).toString())!, + ]); + + assert.deepStrictEqual(t.map(t => t.item.extId.toString()), [ + new TestId(['ctrlId', 'a', 'b1']).toString() + ]); + }); + + test('goes to common root 3', async () => { + const c = await getInitializedMainTestCollection(makeSimpleStubTree(tree1)); + + const t = simplifyTestsToExecute(c, [ + c.getNodeById(new TestId(['ctrlId', 'a', 'b1', 'c1', 'd']).toString())!, + c.getNodeById(new TestId(['ctrlId', 'a', 'b1', 'c2']).toString())!, + ]); + + assert.deepStrictEqual(t.map(t => t.item.extId.toString()), [ + new TestId(['ctrlId', 'a', 'b1']).toString() + ]); + }); + + test('goes to common root 4', async () => { + const c = await getInitializedMainTestCollection(makeSimpleStubTree(tree1)); + + const t = simplifyTestsToExecute(c, [ + c.getNodeById(new TestId(['ctrlId', 'a', 'b2']).toString())!, + c.getNodeById(new TestId(['ctrlId', 'a', 'b1']).toString())!, + ]); + + assert.deepStrictEqual(t.map(t => t.item.extId.toString()), [ + new TestId(['ctrlId']).toString() + ]); + }); + + test('no-op divergent trees', async () => { + const c = await getInitializedMainTestCollection(makeSimpleStubTree(tree1)); + + const t = simplifyTestsToExecute(c, [ + c.getNodeById(new TestId(['ctrlId', 'a', 'b1', 'c2']).toString())!, + c.getNodeById(new TestId(['ctrlId', 'a', 'b2']).toString())!, + ]); + + assert.deepStrictEqual(t.map(t => t.item.extId.toString()), [ + new TestId(['ctrlId', 'a', 'b1', 'c2']).toString(), + new TestId(['ctrlId', 'a', 'b2']).toString(), + ]); + }); + }); +}); diff --git a/src/vs/workbench/contrib/testing/test/common/testStubs.ts b/src/vs/workbench/contrib/testing/test/common/testStubs.ts index 836fe2143e8..c32350bc356 100644 --- a/src/vs/workbench/contrib/testing/test/common/testStubs.ts +++ b/src/vs/workbench/contrib/testing/test/common/testStubs.ts @@ -113,6 +113,26 @@ export const getInitializedMainTestCollection = async (singleUse = testStubs.nes return c; }; +type StubTreeIds = Readonly<{ [id: string]: StubTreeIds | undefined }>; + +export const makeSimpleStubTree = (ids: StubTreeIds): TestTestCollection => { + const collection = new TestTestCollection(); + + const add = (parent: TestTestItem, children: StubTreeIds, path: readonly string[]) => { + for (const id of Object.keys(children)) { + const item = new TestTestItem(new TestId([...path, id]), id); + parent.children.add(item); + if (children[id]) { + add(item, children[id]!, [...path, id]); + } + } + }; + + add(collection.root, ids, ['ctrlId']); + + return collection; +}; + export const testStubs = { nested: (idPrefix = 'id-') => { const collection = new TestTestCollection(); diff --git a/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts b/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts index 6dd030f9b53..6e9ce5b745a 100644 --- a/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; diff --git a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts index ee279e63089..656f9ec5cab 100644 --- a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import { Registry } from 'vs/platform/registry/common/platform'; import { IColorRegistry, Extensions, ColorContribution, asCssVariableName } from 'vs/platform/theme/common/colorRegistry'; import { asTextOrError } from 'vs/platform/request/common/request'; import * as pfs from 'vs/base/node/pfs'; import * as path from 'vs/base/common/path'; -import * as assert from 'assert'; +import assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; import { RequestService } from 'vs/platform/request/node/requestService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -40,7 +41,7 @@ suite('Color Registry', function () { test(`update colors in ${knwonVariablesFileName}`, async function () { const varFilePath = FileAccess.asFileUri(`vs/../../build/lib/stylelint/${knwonVariablesFileName}`).fsPath; - const content = (await pfs.Promises.readFile(varFilePath)).toString(); + const content = (await fs.promises.readFile(varFilePath)).toString(); const variablesInfo = JSON.parse(content); @@ -171,7 +172,7 @@ async function getColorsFromExtension(): Promise<{ [id: string]: string }> { const result: { [id: string]: string } = Object.create(null); for (const folder of extFolders) { try { - const packageJSON = JSON.parse((await pfs.Promises.readFile(path.join(extPath, folder, 'package.json'))).toString()); + const packageJSON = JSON.parse((await fs.promises.readFile(path.join(extPath, folder, 'package.json'))).toString()); const contributes = packageJSON['contributes']; if (contributes) { const colors = contributes['colors']; diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index f152f086e9d..41c6e84efe7 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -574,6 +574,15 @@ export class TimelinePane extends ViewPane { } if (options === undefined) { + if ( + !reset && + timeline !== undefined && + timeline.items.length > 0 && + !timeline.more + ) { + // If we are not resetting, have item(s), and already know there are no more to fetch, we're done here + return false; + } options = { cursor: reset ? undefined : timeline?.cursor, limit: this.pageSize }; } @@ -1306,13 +1315,11 @@ class TimelinePaneCommands extends Disposable { [context.key, context.value], ]); - const menu = this.menuService.createMenu(menuId, contextKeyService); + const menu = this.menuService.getMenuActions(menuId, contextKeyService, { shouldForwardArgs: true }); const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary }; - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, 'inline'); - - menu.dispose(); + createAndFillInContextMenuActions(menu, result, 'inline'); return result; } diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 9f6cc07ba5f..78f429a64bb 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -34,6 +34,7 @@ import { SimpleSettingRenderer } from 'vs/workbench/contrib/markdown/browser/mar import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Schemas } from 'vs/base/common/network'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { marked } from 'vs/base/common/marked/marked'; export class ReleaseNotesManager { private readonly _simpleSettingRenderer: SimpleSettingRenderer; @@ -249,7 +250,10 @@ export class ReleaseNotesManager { private async renderBody(text: string) { const nonce = generateUuid(); - const content = await renderMarkdownDocument(text, this._extensionService, this._languageService, false, undefined, undefined, this._simpleSettingRenderer); + const renderer = new marked.Renderer(); + renderer.html = this._simpleSettingRenderer.getHtmlRenderer(); + + const content = await renderMarkdownDocument(text, this._extensionService, this._languageService, { shouldSanitize: false, renderer }); const colorMap = TokenizationRegistry.getColorMap(); const css = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; const showReleaseNotes = Boolean(this._configurationService.getValue('update.showReleaseNotes')); diff --git a/src/vs/workbench/contrib/update/browser/update.contribution.ts b/src/vs/workbench/contrib/update/browser/update.contribution.ts index fa3edab7d3a..9e34bee7036 100644 --- a/src/vs/workbench/contrib/update/browser/update.contribution.ts +++ b/src/vs/workbench/contrib/update/browser/update.contribution.ts @@ -81,7 +81,6 @@ export class ShowCurrentReleaseNotesFromCurrentFileAction extends Action2 { }, category: localize2('developerCategory', "Developer"), f1: true, - precondition: RELEASE_NOTES_URL }); } diff --git a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts index baab39f82b6..db1c149501f 100644 --- a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts +++ b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css index 0233dfd5309..86a5a8d0119 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css +++ b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css @@ -15,15 +15,26 @@ height: 100%; } -.profiles-editor .contents-container, -.profiles-editor .sidebar-container { +.profiles-editor .monaco-split-view2 > .sash-container, +.profiles-editor .monaco-split-view2.separator-border.horizontal > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before { + top: 55px; +} + +.profiles-editor .contents-container { padding: 0px 20px; height: 100%; } +.profiles-editor .sidebar-container { + padding-left: 20px; + height: 100%; +} + .profiles-editor .sidebar-container .new-profile-button { + padding: 0px 20px 0px 18px; display: flex; align-items: center; + height: 40px; } .profiles-editor .sidebar-container .new-profile-button > .monaco-button-dropdown { @@ -36,20 +47,36 @@ padding: 0 4px; } -.profiles-editor .sidebar-container .profiles-tree { - margin-top: 10px; +.profiles-editor .monaco-list-row .profile-tree-item-actions-container { + display: none; } -.profiles-editor .sidebar-container .profiles-tree .profile-tree-item { +.profiles-editor .monaco-list-row.focused .profile-tree-item-actions-container, +.profiles-editor .monaco-list-row.selected .profile-tree-item-actions-container, +.profiles-editor .monaco-list-row:hover .profile-tree-item-actions-container { display: flex; align-items: center; } -.profiles-editor .sidebar-container .profiles-tree .profile-tree-item > * { +.profiles-editor .sidebar-container .profiles-list { + margin-top: 15px; +} + +.profiles-editor .sidebar-container .profiles-list .profile-list-item { + padding-left: 20px; + display: flex; + align-items: center; +} + +.profiles-editor .sidebar-container .profiles-list .profile-list-item > * { margin-right: 5px; } -.profiles-editor .sidebar-container .profiles-tree .profile-tree-item > .profile-tree-item-description { +.profiles-editor .sidebar-container .profiles-list .profile-list-item > .profile-list-item-label.new-profile { + font-style: italic; +} + +.profiles-editor .sidebar-container .profiles-list .profile-list-item > .profile-list-item-description { margin-left: 2px; display: flex; align-items: center; @@ -57,37 +84,66 @@ opacity: 0.7; } +.profiles-editor .sidebar-container .profiles-list .profile-list-item .profile-tree-item-actions-container { + flex: 1; + justify-content: flex-end; + margin-right: 10px; +} + .profiles-editor .hide { display: none !important; } .profiles-editor .contents-container .profile-header { display: flex; - height: 34px; + height: 40px; align-items: center; } -.profiles-editor .contents-container .profile-header .profile-title { - font-size: x-large; - font-weight: bold; +.profiles-editor .contents-container .profile-header .profile-title-container { flex: 1; + display: flex; + align-items: center; + font-size: medium; +} + +.profiles-editor .contents-container .profile-title-container .codicon { + cursor: pointer; + font-size: large; + padding: 4px; + margin-right: 8px; + border-radius: 5px; +} + +.profiles-editor .contents-container .profile-title-container .codicon.disabled { + cursor: default; +} + +.profiles-editor .contents-container .profile-title-container .codicon:not(.disabled):hover { + background-color: var(--vscode-toolbar-hoverBackground); + outline: 1px dashed var(--vscode-toolbar-hoverOutline); +} + +.profiles-editor .contents-container .profile-title-container .monaco-inputbox { + margin-right: 10px; + flex: 1; +} + +.profiles-editor .contents-container .profile-header .profile-button-container { + display: flex; + align-items: center; +} + +.profiles-editor .contents-container .profile-header .profile-button-container .monaco-button { + margin-left: 4px; } .profiles-editor .contents-container .profile-header .profile-actions-container { display: flex; - height: 28px; -} - -.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container { - gap: 4px; -} - -.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container .codicon { - font-size: 18px; } .profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container { - margin-right: 5px; + margin-right: 6px; min-width: 120px; } @@ -96,28 +152,14 @@ padding-right: 10px; } +.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container .action-label { + padding: 6px; +} + .profiles-editor .contents-container .profile-body { margin-top: 20px; } -.profiles-editor .contents-container .profile-name-container { - margin: 0px 0px 20px 15px; - display: flex; - width: 330px; - align-items: center; -} - -.profiles-editor .contents-container .profile-name-container .codicon { - cursor: pointer; - font-size: 20px; - padding: 2px; -} - -.profiles-editor .contents-container .profile-name-container .monaco-inputbox { - flex: 1; - margin-left: 10px; -} - .profiles-editor .contents-container .profile-select-container { overflow: hidden; display: flex; @@ -127,19 +169,19 @@ .profiles-editor .contents-container .profile-select-container > .monaco-select-box { cursor: pointer; - line-height: 17px; - padding: 2px 23px 2px 8px; + line-height: 18px; + padding: 0px 23px 0px 8px; border-radius: 2px; } .profiles-editor .contents-container .profile-copy-from-container { display: flex; align-items: center; - margin: 0px 0px 20px 20px; + margin: 0px 0px 15px 36px; } .profiles-editor .contents-container .profile-copy-from-container > .profile-copy-from-label { - margin-right: 10px; + margin-right: 25px; display: inline-flex; align-items: center; } @@ -148,26 +190,83 @@ width: 250px; } +.profiles-editor .contents-container .profile-use-as-default-container { + display: flex; + align-items: center; + margin: 0px 20px 15px 6px; + cursor: pointer; +} + +.profiles-editor .contents-container .profile-use-as-default-container .profile-use-as-default-label { + margin-left: 2px; +} + .profiles-editor .contents-container .profile-contents-container { margin: 0px 0px 10px 20px; - font-size: medium; +} + +.profiles-editor .contents-container .profile-content-tree-header, +.profiles-editor .contents-container .profile-content-tree { + margin-left: 6px; +} + +.profiles-editor .contents-container .profile-content-tree-header { + display: grid; + grid-template-columns: 30px repeat(1, 1fr) 150px 100px; + height: 24px; + align-items: center; + margin-bottom: 2px; + background-color: var(--vscode-keybindingTable-headerBackground); + font-weight: bold; } .profiles-editor .contents-container .profile-tree-item-container { - display: flex; + display: grid; align-items: center; } -.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container > .profile-resource-type-label-container { - width: 150px; +.profiles-editor .contents-container .profile-tree-item-container.existing-profile-resource-type-container { + grid-template-columns: repeat(1, 1fr) 150px 100px; } -.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container > .profile-select-container { +.profiles-editor .contents-container .profile-content-tree-header > .inherit-label, +.profiles-editor .contents-container .profile-tree-item-container > .inherit-container { + justify-content: center; + align-items: center; +} + +.profiles-editor .contents-container .profile-tree-item-container > .inherit-container { + padding-left: 50px; +} + +.profiles-editor .contents-container .profile-content-tree-header > .actions-label { + display: flex; + justify-content: center; + align-items: center; +} + +.profiles-editor .contents-container .profile-content-tree-header.new-profile { + grid-template-columns: 30px repeat(2, 1fr) 100px; +} + +.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container { + grid-template-columns: repeat(2, 1fr) 100px; +} + +.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container .profile-select-container { width: 170px; } +.profiles-editor .contents-container .profile-tree-item-container.profile-resource-child-container { + grid-template-columns: repeat(1, 1fr) 100px; +} + .profiles-editor .contents-container .profile-tree-item-container .profile-resource-type-description { margin-left: 10px; font-size: 0.9em; opacity: 0.7; } + +.profiles-editor .contents-container .profile-tree-item-container .profile-tree-item-actions-container { + justify-content: center; +} diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index 5ca000e81a7..29d96138089 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -18,7 +18,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { URI } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags'; import { getErrorMessage } from 'vs/base/common/errors'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; @@ -30,9 +30,19 @@ import { UserDataProfilesEditor, UserDataProfilesEditorInput, UserDataProfilesEd import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IUserDataProfilesEditor } from 'vs/workbench/contrib/userDataProfile/common/userDataProfile'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { IProductService } from 'vs/platform/product/common/productService'; type IProfileTemplateQuickPickItem = IQuickPickItem & IProfileTemplateInfo; +export const OpenProfileMenu = new MenuId('OpenProfile'); +const CONFIG_ENABLE_NEW_PROFILES_UI = 'workbench.experimental.enableNewProfilesUI'; +const CONTEXT_ENABLE_NEW_PROFILES_UI = ContextKeyExpr.equals('config.workbench.experimental.enableNewProfilesUI', true); + export class UserDataProfilesWorkbenchContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.userDataProfiles'; @@ -40,6 +50,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements private readonly currentProfileContext: IContextKey; private readonly isCurrentProfileTransientContext: IContextKey; private readonly hasProfilesContext: IContextKey; + private readonly startTime: number = Date.now(); constructor( @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @@ -50,6 +61,10 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService, @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IProductService private readonly productService: IProductService, @ILifecycleService private readonly lifecycleService: ILifecycleService, ) { super(); @@ -69,6 +84,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1); this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1))); + this.registerConfiguration(); this.registerEditor(); this.registerActions(); @@ -79,6 +95,29 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.reportWorkspaceProfileInfo(); } + private openProfilesEditor(): Promise { + return this.editorGroupsService.activeGroup.openEditor(new UserDataProfilesEditorInput(this.instantiationService)); + } + + private isNewProfilesUIEnabled(): boolean { + return this.configurationService.getValue(CONFIG_ENABLE_NEW_PROFILES_UI) === true; + } + + private registerConfiguration(): void { + Registry.as(Extensions.Configuration) + .registerConfiguration({ + ...workbenchConfigurationNodeBase, + properties: { + [CONFIG_ENABLE_NEW_PROFILES_UI]: { + type: 'boolean', + description: localize('enable new profiles UI', "Enables the new profiles UI."), + default: this.productService.quality !== 'stable', + scope: ConfigurationScope.APPLICATION, + } + } + }); + } + private registerEditor(): void { Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( @@ -98,6 +137,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this._register(this.registerManageProfilesAction()); this._register(this.registerSwitchProfileAction()); + this.registerOpenProfileSubMenu(); this.registerProfilesActions(); this._register(this.userDataProfilesService.onDidChangeProfiles(() => this.registerProfilesActions())); @@ -115,6 +155,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements const getProfilesTitle = () => { return localize('profiles', "Profile ({0})", this.userDataProfileService.currentProfile.name); }; + const when = ContextKeyExpr.or(CONTEXT_ENABLE_NEW_PROFILES_UI.negate(), HAS_PROFILES_CONTEXT); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { get title() { return getProfilesTitle(); @@ -122,6 +163,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements submenu: ProfilesMenu, group: '2_configuration', order: 1, + when, }); MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { get title() { @@ -130,60 +172,28 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements submenu: ProfilesMenu, group: '2_configuration', order: 1, - when: PROFILES_ENABLEMENT_CONTEXT, + when, }); } - private registerManageProfilesAction(): IDisposable { - const disposables = new DisposableStore(); - const when = ContextKeyExpr.equals('config.workbench.experimental.enableNewProfilesUI', true); - disposables.add(registerAction2(class ManageProfilesAction extends Action2 { - constructor() { - super({ - id: `workbench.profiles.actions.manageProfiles`, - title: { - ...localize2('manage profiles', "Profiles"), - mnemonicTitle: localize({ key: 'miOpenProfiles', comment: ['&& denotes a mnemonic'] }, "&&Profiles"), - }, - menu: [ - { - id: MenuId.GlobalActivity, - group: '2_configuration', - when, - order: 1 - }, - { - id: MenuId.MenubarPreferencesMenu, - group: '2_configuration', - when, - order: 1 - } - ] - }); - } - run(accessor: ServicesAccessor) { - const editorGroupsService = accessor.get(IEditorGroupsService); - const instantiationService = accessor.get(IInstantiationService); - return editorGroupsService.activeGroup.openEditor(new UserDataProfilesEditorInput(instantiationService)); - } - })); - disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: 'workbench.profiles.actions.manageProfiles', - category: Categories.Preferences, - title: localize2('open profiles', "Open Profiles (UI)"), - precondition: when, - }, - })); - - return disposables; + private registerOpenProfileSubMenu(): void { + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + title: localize('New Profile Window', "New Window with Profile"), + submenu: OpenProfileMenu, + group: '1_new', + order: 4, + when: HAS_PROFILES_CONTEXT, + }); } private readonly profilesDisposable = this._register(new MutableDisposable()); private registerProfilesActions(): void { this.profilesDisposable.value = new DisposableStore(); for (const profile of this.userDataProfilesService.profiles) { - this.profilesDisposable.value.add(this.registerProfileEntryAction(profile)); + if (!profile.isTransient) { + this.profilesDisposable.value.add(this.registerProfileEntryAction(profile)); + this.profilesDisposable.value.add(this.registerNewWindowAction(profile)); + } } } @@ -206,12 +216,61 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } async run(accessor: ServicesAccessor) { if (that.userDataProfileService.currentProfile.id !== profile.id) { + if (profile.isDefault && Date.now() - that.startTime < (1000 * 20 /* 20 seconds */)) { + type SwitchToDefaultProfileInfoClassification = { + owner: 'sandy081'; + comment: 'Report if the user switches to the default profile.'; + emptyWindow: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If the current window is empty window or not' }; + }; + type SwitchToDefaultProfileInfoEvent = { + emptyWindow: boolean; + }; + that.telemetryService.publicLog2('profiles:newwindowprofile', { + emptyWindow: that.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY + }); + } return that.userDataProfileManagementService.switchProfile(profile); } } }); } + private registerNewWindowAction(profile: IUserDataProfile): IDisposable { + const disposables = new DisposableStore(); + + const id = `workbench.action.openProfile.${profile.name.toLowerCase().replace('/\s+/', '_')}`; + + disposables.add(registerAction2(class NewWindowAction extends Action2 { + + constructor() { + super({ + id, + title: localize2('openShort', "{0}", profile.name), + menu: { + id: OpenProfileMenu, + when: HAS_PROFILES_CONTEXT + } + }); + } + + override run(accessor: ServicesAccessor): Promise { + const hostService = accessor.get(IHostService); + return hostService.openWindow({ remoteAuthority: null, forceProfile: profile.name }); + } + })); + + disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id, + category: PROFILES_CATEGORY, + title: localize2('open', "Open {0} Profile", profile.name), + precondition: HAS_PROFILES_CONTEXT + }, + })); + + return disposables; + } + private registerSwitchProfileAction(): IDisposable { return registerAction2(class SwitchProfileAction extends Action2 { constructor() { @@ -226,19 +285,16 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements async run(accessor: ServicesAccessor) { const quickInputService = accessor.get(IQuickInputService); const menuService = accessor.get(IMenuService); - const menu = menuService.createMenu(ProfilesMenu, accessor.get(IContextKeyService)); - const actions = menu.getActions().find(([group]) => group === '0_profiles')?.[1] ?? []; - try { - const result = await quickInputService.pick(actions.map(action => ({ - action, - label: action.checked ? `$(check) ${action.label}` : action.label, - })), { - placeHolder: localize('selectProfile', "Select Profile") - }); - await result?.action.run(); - } finally { - menu.dispose(); - } + const menu = menuService.getMenuActions(ProfilesMenu, accessor.get(IContextKeyService)); + const actions = menu.find(([group]) => group === '0_profiles')?.[1] ?? []; + const result = await quickInputService.pick(actions.map(action => ({ + action, + label: action.checked ? `$(check) ${action.label}` : action.label, + })), { + placeHolder: localize('selectProfile', "Select Profile") + }); + await result?.action.run(); + } }); } @@ -252,28 +308,76 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.currentprofileActionsDisposable.value.add(this.registerImportProfileAction()); } + private registerManageProfilesAction(): IDisposable { + const disposables = new DisposableStore(); + disposables.add(registerAction2(class ManageProfilesAction extends Action2 { + constructor() { + super({ + id: `workbench.profiles.actions.manageProfiles`, + title: { + ...localize2('manage profiles', "Profiles"), + mnemonicTitle: localize({ key: 'miOpenProfiles', comment: ['&& denotes a mnemonic'] }, "&&Profiles"), + }, + menu: [ + { + id: MenuId.GlobalActivity, + group: '2_configuration', + order: 1, + when: CONTEXT_ENABLE_NEW_PROFILES_UI, + }, + { + id: MenuId.MenubarPreferencesMenu, + group: '2_configuration', + order: 1, + when: CONTEXT_ENABLE_NEW_PROFILES_UI, + }, + ] + }); + } + run(accessor: ServicesAccessor) { + const editorGroupsService = accessor.get(IEditorGroupsService); + const instantiationService = accessor.get(IInstantiationService); + return editorGroupsService.activeGroup.openEditor(new UserDataProfilesEditorInput(instantiationService)); + } + })); + disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.profiles.actions.manageProfiles', + category: Categories.Preferences, + title: localize2('open profiles', "Open Profiles (UI)"), + precondition: CONTEXT_ENABLE_NEW_PROFILES_UI, + }, + })); + + return disposables; + } + private registerEditCurrentProfileAction(): IDisposable { const that = this; return registerAction2(class RenameCurrentProfileAction extends Action2 { constructor() { - const when = ContextKeyExpr.and(ContextKeyExpr.notEquals(CURRENT_PROFILE_CONTEXT.key, that.userDataProfilesService.defaultProfile.id), IS_CURRENT_PROFILE_TRANSIENT_CONTEXT.toNegated()); + const precondition = ContextKeyExpr.and(ContextKeyExpr.notEquals(CURRENT_PROFILE_CONTEXT.key, that.userDataProfilesService.defaultProfile.id), IS_CURRENT_PROFILE_TRANSIENT_CONTEXT.toNegated()); super({ id: `workbench.profiles.actions.editCurrentProfile`, title: localize2('edit profile', "Edit Profile..."), - precondition: when, + precondition, f1: true, menu: [ { id: ProfilesMenu, group: '2_manage_current', - when, + when: ContextKeyExpr.and(precondition, CONTEXT_ENABLE_NEW_PROFILES_UI.negate()), order: 2 } ] }); } - run() { - return that.userDataProfileImportExportService.editProfile(that.userDataProfileService.currentProfile); + run(accessor: ServicesAccessor) { + if (that.isNewProfilesUIEnabled()) { + return that.openProfilesEditor(); + } else { + return that.userDataProfileImportExportService.editProfile(that.userDataProfileService.currentProfile); + } } }); } @@ -290,9 +394,8 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements { id: ProfilesMenu, group: '2_manage_current', - order: 3 - }, { - id: MenuId.CommandPalette + order: 3, + when: CONTEXT_ENABLE_NEW_PROFILES_UI.negate() } ] }); @@ -320,7 +423,8 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements { id: ProfilesMenu, group: '4_import_export_profiles', - order: 1 + order: 1, + when: CONTEXT_ENABLE_NEW_PROFILES_UI.negate(), }, { id: MenuId.CommandPalette } @@ -329,8 +433,11 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } async run(accessor: ServicesAccessor) { - const userDataProfileImportExportService = accessor.get(IUserDataProfileImportExportService); - return userDataProfileImportExportService.exportProfile(); + if (that.isNewProfilesUIEnabled()) { + return that.openProfilesEditor(); + } else { + return that.userDataProfileImportExportService.exportProfile2(); + } } })); disposables.add(MenuRegistry.appendMenuItem(MenuId.MenubarShare, { @@ -358,7 +465,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements { id: ProfilesMenu, group: '4_import_export_profiles', - when: PROFILES_ENABLEMENT_CONTEXT, + when: ContextKeyExpr.and(PROFILES_ENABLEMENT_CONTEXT, CONTEXT_ENABLE_NEW_PROFILES_UI.negate()), order: 2 }, { id: MenuId.CommandPalette, @@ -479,7 +586,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements { id: ProfilesMenu, group: '3_manage_profiles', - when: PROFILES_ENABLEMENT_CONTEXT, + when: ContextKeyExpr.and(PROFILES_ENABLEMENT_CONTEXT, CONTEXT_ENABLE_NEW_PROFILES_UI.negate()), order: 1 } ] @@ -487,7 +594,11 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } async run(accessor: ServicesAccessor) { - return that.userDataProfileImportExportService.createProfile(); + if (that.isNewProfilesUIEnabled()) { + return that.openProfilesEditor(); + } else { + return that.userDataProfileImportExportService.createProfile(); + } } })); } @@ -505,7 +616,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements { id: ProfilesMenu, group: '3_manage_profiles', - when: PROFILES_ENABLEMENT_CONTEXT, + when: ContextKeyExpr.and(PROFILES_ENABLEMENT_CONTEXT, CONTEXT_ENABLE_NEW_PROFILES_UI.negate()), order: 2 } ] diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfileActions.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfileActions.ts index a9b85a44e65..c19f31930f8 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfileActions.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfileActions.ts @@ -120,10 +120,9 @@ registerAction2(class ManageProfilesAction extends Action2 { const contextKeyService = accessor.get(IContextKeyService); const commandService = accessor.get(ICommandService); - const menu = menuService.createMenu(ProfilesMenu, contextKeyService); + const menu = menuService.getMenuActions(ProfilesMenu, contextKeyService); const actions: IAction[] = []; - createAndFillInActionBarActions(menu, undefined, actions); - menu.dispose(); + createAndFillInActionBarActions(menu, actions); if (actions.length) { const picks: QuickPickItem[] = actions.map(action => { diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index 4c294d03ac2..f1075952370 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -20,15 +20,15 @@ import { IEditorOpenContext, IEditorSerializer, IUntypedEditorInput } from 'vs/w import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IUserDataProfilesEditor } from 'vs/workbench/contrib/userDataProfile/common/userDataProfile'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { defaultUserDataProfileIcon, IProfileResourceChildTreeItem, IProfileTemplateInfo, IUserDataProfileManagementService, PROFILE_FILTER } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { defaultUserDataProfileIcon, IProfileTemplateInfo, IUserDataProfileManagementService, PROFILE_FILTER } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { Button, ButtonWithDropdown } from 'vs/base/browser/ui/button/button'; import { defaultButtonStyles, defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; -import { WorkbenchAsyncDataTree, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IAsyncDataSource, IObjectTreeElement, ITreeNode, ITreeRenderer, ObjectTreeElementCollapseState } from 'vs/base/browser/ui/tree/tree'; +import { WorkbenchAsyncDataTree, WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; @@ -44,23 +44,19 @@ import { IHoverWidget } from 'vs/base/browser/ui/hover/hover'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { URI } from 'vs/base/common/uri'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; -import { ExtensionsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/extensionsResource'; -import { isString, isUndefined } from 'vs/base/common/types'; +import { isUndefined } from 'vs/base/common/types'; import { basename } from 'vs/base/common/resources'; import { RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { DEFAULT_LABELS_CONTAINER, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { AbstractUserDataProfileElement, IProfileElement, NewProfileElement, UserDataProfileElement, UserDataProfilesEditorModel } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel'; +import { AbstractUserDataProfileElement, isProfileResourceChildElement, isProfileResourceTypeElement, IProfileChildElement, IProfileResourceTypeChildElement, IProfileResourceTypeElement, NewProfileElement, UserDataProfileElement, UserDataProfilesEditorModel } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel'; import { Codicon } from 'vs/base/common/codicons'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -export const profilesSashBorder = registerColor('profiles.sashBorder', { dark: PANEL_BORDER, light: PANEL_BORDER, hcDark: PANEL_BORDER, hcLight: PANEL_BORDER }, localize('profilesSashBorder', "The color of the Profiles editor splitview sash border.")); +export const profilesSashBorder = registerColor('profiles.sashBorder', PANEL_BORDER, localize('profilesSashBorder', "The color of the Profiles editor splitview sash border.")); export class UserDataProfilesEditor extends EditorPane implements IUserDataProfilesEditor { @@ -68,7 +64,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi private container: HTMLElement | undefined; private splitView: SplitView | undefined; - private profilesTree: WorkbenchObjectTree | undefined; + private profilesList: WorkbenchList | undefined; private profileWidget: ProfileWidget | undefined; private model: UserDataProfilesEditorModel | undefined; @@ -81,7 +77,6 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi @IStorageService storageService: IStorageService, @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IDialogService private readonly dialogService: IDialogService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -116,20 +111,21 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi this.splitView.addView({ onDidChange: Event.None, element: sidebarView, - minimumSize: 175, + minimumSize: 200, maximumSize: 350, layout: (width, _, height) => { sidebarView.style.width = `${width}px`; - if (height && this.profilesTree) { - this.profilesTree.getHTMLElement().style.height = `${height - 38}px`; - this.profilesTree.layout(height - 38, width); + if (height && this.profilesList) { + const listHeight = height - 40 /* new profile button */ - 15 /* marginTop */; + this.profilesList.getHTMLElement().style.height = `${listHeight}px`; + this.profilesList.layout(listHeight, width); } } }, 300, undefined, true); this.splitView.addView({ onDidChange: Event.None, element: contentsView, - minimumSize: 500, + minimumSize: 550, maximumSize: Number.POSITIVE_INFINITY, layout: (width, _, height) => { contentsView.style.width = `${width}px`; @@ -139,10 +135,8 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi } }, Sizing.Distribute, undefined, true); - const borderColor = this.theme.getColor(profilesSashBorder)!; - this.splitView.style({ separatorBorder: borderColor }); - this.registerListeners(); + this.updateStyles(); this.userDataProfileManagementService.getBuiltinProfileTemplates().then(templates => { this.templates = templates; @@ -150,15 +144,20 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi }); } + override updateStyles(): void { + const borderColor = this.theme.getColor(profilesSashBorder)!; + this.splitView?.style({ separatorBorder: borderColor }); + } + private renderSidebar(parent: HTMLElement): void { // render New Profile Button this.renderNewProfileButton(append(parent, $('.new-profile-button'))); - // render profiles and templates tree - const renderer = this.instantiationService.createInstance(ProfileTreeElementRenderer); - const delegate = new ProfileTreeElementDelegate(); - this.profilesTree = this._register(this.instantiationService.createInstance(WorkbenchObjectTree, 'ProfilesTree', - append(parent, $('.profiles-tree')), + // render profiles list + const renderer = this.instantiationService.createInstance(ProfileElementRenderer); + const delegate = new ProfileElementDelegate(); + this.profilesList = this._register(this.instantiationService.createInstance(WorkbenchList, 'ProfilesList', + append(parent, $('.profiles-list')), delegate, [renderer], { @@ -166,15 +165,14 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi setRowLineHeight: false, horizontalScrolling: false, accessibilityProvider: { - getAriaLabel(extensionFeature: IProfileElement | null): string { - return extensionFeature?.name ?? ''; + getAriaLabel(profileElement: AbstractUserDataProfileElement | null): string { + return profileElement?.name ?? ''; }, getWidgetAriaLabel(): string { return localize('profiles', "Profiles"); } }, openOnSingleClick: true, - enableStickyScroll: false, identityProvider: { getId(e) { if (e instanceof UserDataProfileElement) { @@ -192,10 +190,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi getActions: () => { const actions: IAction[] = []; if (this.templates.length) { - actions.push(new SubmenuAction('from.template', localize('from template', "From Template"), - this.templates.map(template => new Action(`template:${template.url}`, template.name, undefined, true, async () => { - this.createNewProfile(URI.parse(template.url)); - })))); + actions.push(new SubmenuAction('from.template', localize('from template', "From Template"), this.getCreateFromTemplateActions())); actions.push(new Separator()); } actions.push(new Action('importProfile', localize('importProfile', "Import Profile..."), undefined, true, () => this.importProfile())); @@ -207,31 +202,60 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi supportIcons: true, ...defaultButtonStyles })); - button.label = `$(add) ${localize('newProfile', "New Profile")}`; + button.label = localize('newProfile', "New Profile"); this._register(button.onDidClick(e => this.createNewProfile())); } + private getCreateFromTemplateActions(): IAction[] { + return this.templates.map(template => new Action(`template:${template.url}`, template.name, undefined, true, async () => { + this.createNewProfile(URI.parse(template.url)); + })); + } + private registerListeners(): void { - if (this.profilesTree) { - this._register(this.profilesTree.onDidChangeSelection(e => { + if (this.profilesList) { + this._register(this.profilesList.onDidChangeSelection(e => { const [element] = e.elements; if (element instanceof AbstractUserDataProfileElement) { this.profileWidget?.render(element); } })); - - this._register(this.profilesTree.onContextMenu(e => { + this._register(this.profilesList.onContextMenu(e => { + const actions: IAction[] = []; + if (!e.element) { + actions.push(...this.getTreeContextMenuActions()); + } if (e.element instanceof AbstractUserDataProfileElement) { + actions.push(...e.element.actions[1]); + } + if (actions.length) { this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, - getActions: () => e.element instanceof AbstractUserDataProfileElement ? e.element.contextMenuActions.slice(0) : [], + getActions: () => actions, getActionsContext: () => e.element }); } })); + this._register(this.profilesList.onMouseDblClick(e => { + if (!e.element) { + this.createNewProfile(); + } + })); } } + private getTreeContextMenuActions(): IAction[] { + const actions: IAction[] = []; + actions.push(new Action('newProfile', localize('newProfile', "New Profile"), undefined, true, () => this.createNewProfile())); + const templateActions = this.getCreateFromTemplateActions(); + if (templateActions.length) { + actions.push(new SubmenuAction('from.template', localize('new from template', "New Profile From Template"), templateActions)); + } + actions.push(new Separator()); + actions.push(new Action('importProfile', localize('importProfile', "Import Profile..."), undefined, true, () => this.importProfile())); + return actions; + } + private async importProfile(): Promise { const disposables = new DisposableStore(); const quickPick = disposables.add(this.quickInputService.createQuickPick()); @@ -268,19 +292,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi } private async createNewProfile(copyFrom?: URI | IUserDataProfile): Promise { - if (this.model?.profiles.some(p => p instanceof NewProfileElement)) { - const result = await this.dialogService.confirm({ - type: 'info', - message: localize('new profile exists', "A new profile is already being created. Do you want to discard it and create a new one?"), - primaryButton: localize('discard', "Discard & Create"), - cancelButton: localize('cancel', "Cancel") - }); - if (!result.confirmed) { - return; - } - this.model.revert(); - } - this.model?.createNewProfile(copyFrom); + await this.model?.createNewProfile(copyFrom); } private async getProfileUriFromFileSystem(): Promise { @@ -300,91 +312,100 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi override async setInput(input: UserDataProfilesEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); this.model = await input.resolve(); - this.updateProfilesTree(); + this.updateProfilesList(); this._register(this.model.onDidChange((element) => { - this.updateProfilesTree(element); + this.updateProfilesList(element); })); } override focus(): void { super.focus(); - this.profilesTree?.domFocus(); + this.profilesList?.domFocus(); } - private updateProfilesTree(elementToSelect?: IProfileElement): void { + private updateProfilesList(elementToSelect?: AbstractUserDataProfileElement): void { if (!this.model) { return; } - const profileElements: IObjectTreeElement[] = this.model.profiles.map(element => ({ element })); - const currentSelection = this.profilesTree?.getSelection()?.[0]; - this.profilesTree?.setChildren(null, [ - { - element: { name: localize('profiles', "Profiles") }, - children: profileElements, - collapsible: false, - collapsed: ObjectTreeElementCollapseState.Expanded - } - ]); + const currentSelectionIndex = this.profilesList?.getSelection()?.[0]; + const currentSelection = currentSelectionIndex !== undefined ? this.profilesList?.element(currentSelectionIndex) : undefined; + this.profilesList?.splice(0, this.profilesList.length, this.model.profiles); + if (elementToSelect) { - this.profilesTree?.setSelection([elementToSelect]); + this.profilesList?.setSelection([this.model.profiles.indexOf(elementToSelect)]); } else if (currentSelection) { - if (currentSelection instanceof AbstractUserDataProfileElement) { - if (!this.model.profiles.includes(currentSelection)) { - const elementToSelect = this.model.profiles.find(profile => profile.name === currentSelection.name) ?? this.model.profiles[0]; - if (elementToSelect) { - this.profilesTree?.setSelection([elementToSelect]); - } + if (!this.model.profiles.includes(currentSelection)) { + const elementToSelect = this.model.profiles.find(profile => profile.name === currentSelection.name) ?? this.model.profiles[0]; + if (elementToSelect) { + this.profilesList?.setSelection([this.model.profiles.indexOf(elementToSelect)]); } } } else { const elementToSelect = this.model.profiles.find(profile => profile.active) ?? this.model.profiles[0]; if (elementToSelect) { - this.profilesTree?.setSelection([elementToSelect]); + this.profilesList?.setSelection([this.model.profiles.indexOf(elementToSelect)]); } } } } -interface IProfileTreeElementTemplateData { +interface IProfileElementTemplateData { readonly icon: HTMLElement; readonly label: HTMLElement; readonly description: HTMLElement; + readonly actionBar: WorkbenchToolBar; readonly disposables: DisposableStore; + readonly elementDisposables: DisposableStore; } -class ProfileTreeElementDelegate implements IListVirtualDelegate { - getHeight(element: IProfileElement) { - return 30; +class ProfileElementDelegate implements IListVirtualDelegate { + getHeight(element: AbstractUserDataProfileElement) { + return 22; } - getTemplateId() { return 'profileTreeElement'; } + getTemplateId() { return 'profileListElement'; } } -class ProfileTreeElementRenderer implements ITreeRenderer { +class ProfileElementRenderer implements IListRenderer { - readonly templateId = 'profileTreeElement'; + readonly templateId = 'profileListElement'; - renderTemplate(container: HTMLElement): IProfileTreeElementTemplateData { - container.classList.add('profile-tree-item'); - const icon = append(container, $('.profile-tree-item-icon')); - const label = append(container, $('.profile-tree-item-label')); - const description = append(container, $('.profile-tree-item-description')); + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + renderTemplate(container: HTMLElement): IProfileElementTemplateData { + + const disposables = new DisposableStore(); + const elementDisposables = new DisposableStore(); + + container.classList.add('profile-list-item'); + const icon = append(container, $('.profile-list-item-icon')); + const label = append(container, $('.profile-list-item-label')); + const description = append(container, $('.profile-list-item-description')); append(description, $(`span${ThemeIcon.asCSSSelector(Codicon.check)}`)); - append(description, $('span', undefined, localize('activeProfile', "Active"))); - return { label, icon, description, disposables: new DisposableStore() }; + append(description, $('span', undefined, localize('activeProfile', "In use"))); + + const actionsContainer = append(container, $('.profile-tree-item-actions-container')); + const actionBar = disposables.add(this.instantiationService.createInstance(WorkbenchToolBar, + actionsContainer, + { + hoverDelegate: disposables.add(createInstantHoverDelegate()), + highlightToggledItems: true + } + )); + + return { label, icon, description, actionBar, disposables, elementDisposables }; } - renderElement({ element }: ITreeNode, index: number, templateData: IProfileTreeElementTemplateData, height: number | undefined): void { - templateData.disposables.clear(); + renderElement(element: AbstractUserDataProfileElement, index: number, templateData: IProfileElementTemplateData, height: number | undefined) { + templateData.elementDisposables.clear(); templateData.label.textContent = element.name; - if (element.icon) { - templateData.icon.className = ThemeIcon.asClassName(ThemeIcon.fromId(element.icon)); - } else { - templateData.icon.className = 'hide'; - } + templateData.label.classList.toggle('new-profile', element instanceof NewProfileElement); + templateData.icon.className = ThemeIcon.asClassName(element.icon ? ThemeIcon.fromId(element.icon) : DEFAULT_ICON); templateData.description.classList.toggle('hide', !element.active); if (element.onDidChange) { - templateData.disposables.add(element.onDidChange(e => { + templateData.elementDisposables.add(element.onDidChange(e => { if (e.name) { templateData.label.textContent = element.name; } @@ -400,10 +421,16 @@ class ProfileTreeElementRenderer implements ITreeRenderer; private _templates: IProfileTemplateInfo[] = []; @@ -435,32 +465,18 @@ class ProfileWidget extends Disposable { @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IContextViewService private readonly contextViewService: IContextViewService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, - @ICommandService private readonly commandService: ICommandService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); const header = append(parent, $('.profile-header')); - const title = append(header, $('.profile-title')); - append(title, $('span', undefined, localize('profile', "Profile: "))); - this.profileTitle = append(title, $('span')); - const actionsContainer = append(header, $('.profile-actions-container')); - this.buttonContainer = append(actionsContainer, $('.profile-button-container')); - this.toolbar = this._register(instantiationService.createInstance(WorkbenchToolBar, - actionsContainer, - { - hoverDelegate: this._register(createInstantHoverDelegate()), - } - )); - - const body = append(parent, $('.profile-body')); - - this.nameContainer = append(body, $('.profile-name-container')); - this.iconElement = append(this.nameContainer, $(`${ThemeIcon.asCSSSelector(DEFAULT_ICON)}`, { 'tabindex': '0', 'role': 'button', 'aria-label': localize('icon', "Profile Icon") })); + const title = append(header, $('.profile-title-container')); + this.iconElement = append(title, $(`${ThemeIcon.asCSSSelector(DEFAULT_ICON)}`, { 'tabindex': '0', 'role': 'button', 'aria-label': localize('icon', "Profile Icon") })); this.renderIconSelectBox(this.iconElement); + this.profileTitle = append(title, $('')); this.nameInput = this._register(new InputBox( - this.nameContainer, + title, undefined, { inputBoxStyles: defaultInputBoxStyles, @@ -474,7 +490,11 @@ class ProfileWidget extends Disposable { type: MessageType.ERROR }; } - const initialName = this._profileElement.value?.element instanceof UserDataProfileElement ? this._profileElement.value.element.profile.name : undefined; + if (this._profileElement.value?.element.disabled) { + return null; + } + const initialName = this._profileElement.value?.element.getInitialName(); + value = value.trim(); if (initialName !== value && this.userDataProfilesService.profiles.some(p => p.name === value)) { return { content: localize('profileExists', "Profile with name {0} already exists.", value), @@ -498,8 +518,20 @@ class ProfileWidget extends Disposable { } })); + const actionsContainer = append(header, $('.profile-actions-container')); + this.buttonContainer = append(actionsContainer, $('.profile-button-container')); + this.toolbar = this._register(instantiationService.createInstance(WorkbenchToolBar, + actionsContainer, + { + hoverDelegate: this._register(createInstantHoverDelegate()), + highlightToggledItems: true + } + )); + + const body = append(parent, $('.profile-body')); + this.copyFromContainer = append(body, $('.profile-copy-from-container')); - append(this.copyFromContainer, $('.profile-copy-from-label', undefined, localize('create from', "Copy from:"))); + append(this.copyFromContainer, $('.profile-copy-from-label', undefined, localize('create from', "Copy from"))); this.copyFromSelectBox = this._register(this.instantiationService.createInstance(SelectBox, [], 0, @@ -512,9 +544,30 @@ class ProfileWidget extends Disposable { )); this.copyFromSelectBox.render(append(this.copyFromContainer, $('.profile-select-container'))); - const contentsContainer = append(body, $('.profile-contents-container')); - append(contentsContainer, $('.profile-contents-label', undefined, localize('contents', "Contents"))); + this.useAsDefaultProfileContainer = append(body, $('.profile-use-as-default-container')); + const useAsDefaultProfileTitle = localize('enable for new windows', "Use this profile as default for new windows"); + this.useAsDefaultProfileCheckbox = this._register(new Checkbox(useAsDefaultProfileTitle, false, defaultCheckboxStyles)); + append(this.useAsDefaultProfileContainer, this.useAsDefaultProfileCheckbox.domNode); + const useAsDefaultProfileLabel = append(this.useAsDefaultProfileContainer, $('.profile-use-as-default-label', undefined, useAsDefaultProfileTitle)); + this._register(this.useAsDefaultProfileCheckbox.onChange(() => { + if (this._profileElement.value?.element instanceof UserDataProfileElement) { + this._profileElement.value.element.toggleNewWindowProfile(); + } + })); + this._register(addDisposableListener(useAsDefaultProfileLabel, EventType.CLICK, () => { + if (this._profileElement.value?.element instanceof UserDataProfileElement) { + this._profileElement.value.element.toggleNewWindowProfile(); + } + })); + this.contentsTreeHeader = append(body, $('.profile-content-tree-header')); + this.inheritLabelElement = $('.inherit-label', undefined, localize('default profile', "Use Default Profile")); + append(this.contentsTreeHeader, + $(''), + $(''), + this.inheritLabelElement, + $('.actions-label', undefined, localize('actions', "Actions")), + ); const delegate = new ProfileResourceTreeElementDelegate(); this.resourcesTree = this._register(this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'ProfileEditor-ResourcesTree', @@ -531,11 +584,11 @@ class ProfileWidget extends Disposable { horizontalScrolling: false, accessibilityProvider: { getAriaLabel(element: ProfileResourceTreeElement | null): string { - if (isString(element?.element)) { - return element.element; + if ((element?.element).resourceType) { + return (element?.element).resourceType; } - if (element?.element) { - return element.element.label?.label ?? ''; + if ((element?.element).label) { + return (element?.element).label; } return ''; }, @@ -545,10 +598,7 @@ class ProfileWidget extends Disposable { }, identityProvider: { getId(element) { - if (isString(element?.element)) { - return element.element; - } - if (element?.element) { + if (element?.element.handle) { return element.element.handle; } return ''; @@ -556,8 +606,8 @@ class ProfileWidget extends Disposable { }, expandOnlyOnTwistieClick: true, renderIndentGuides: RenderIndentGuides.None, - openOnSingleClick: true, enableStickyScroll: false, + openOnSingleClick: false, })); this._register(this.resourcesTree.onDidOpen(async (e) => { if (!e.browserEvent) { @@ -566,12 +616,8 @@ class ProfileWidget extends Disposable { if (e.browserEvent.target && (e.browserEvent.target as HTMLElement).classList.contains(Checkbox.CLASS_NAME)) { return; } - if (e.element && !isString(e.element.element)) { - if (e.element.element.resourceUri) { - await this.commandService.executeCommand(API_OPEN_EDITOR_COMMAND_ID, e.element.element.resourceUri, [SIDE_GROUP], undefined, e); - } else if (e.element.element.parent instanceof ExtensionsResourceTreeItem) { - await this.commandService.executeCommand('extension.open', e.element.element.handle, undefined, true, undefined, true); - } + if (e.element?.element.action) { + await e.element.element.action.run(); } })); } @@ -583,6 +629,9 @@ class ProfileWidget extends Disposable { if (this._profileElement.value?.element instanceof UserDataProfileElement && this._profileElement.value.element.profile.isDefault) { return; } + if (this._profileElement.value?.element.disabled) { + return; + } iconSelectBox.clearInput(); hoverWidget = this.hoverService.showHover({ content: iconSelectBox.domNode, @@ -632,19 +681,7 @@ class ProfileWidget extends Disposable { } private renderSelectBox(): void { - const separator = { text: '\u2500\u2500\u2500\u2500\u2500\u2500', isDisabled: true }; - this.copyFromOptions.push({ text: localize('empty profile', "None") }); - if (this._templates.length) { - this.copyFromOptions.push({ ...separator, decoratorRight: localize('from templates', "Profile Templates") }); - for (const template of this._templates) { - this.copyFromOptions.push({ text: template.name, id: template.url, source: URI.parse(template.url) }); - } - } - this.copyFromOptions.push({ ...separator, decoratorRight: localize('from existing profiles', "Existing Profiles") }); - for (const profile of this.userDataProfilesService.profiles) { - this.copyFromOptions.push({ text: profile.name, id: profile.id, source: profile }); - } - this.copyFromSelectBox.setOptions(this.copyFromOptions); + this.copyFromSelectBox.setOptions(this.getCopyFromOptions()); this._register(this.copyFromSelectBox.onDidSelect(option => { if (this._profileElement.value?.element instanceof NewProfileElement) { this._profileElement.value.element.copyFrom = this.copyFromOptions[option.index].source; @@ -657,6 +694,8 @@ class ProfileWidget extends Disposable { } render(profileElement: AbstractUserDataProfileElement): void { + this.resourcesTree.setInput(profileElement); + const disposables = new DisposableStore(); this._profileElement.value = { element: profileElement, dispose: () => disposables.dispose() }; @@ -664,11 +703,13 @@ class ProfileWidget extends Disposable { disposables.add(profileElement.onDidChange(e => this.renderProfileElement(profileElement))); const profile = profileElement instanceof UserDataProfileElement ? profileElement.profile : undefined; - this.nameInput.setEnabled(!profile?.isDefault); + this.profileTitle.classList.toggle('hide', !profile?.isDefault); + this.nameInput.element.classList.toggle('hide', !!profile?.isDefault); + this.iconElement.classList.toggle('disabled', !!profile?.isDefault); + this.iconElement.setAttribute('tabindex', profile?.isDefault ? '' : '0'); - this.resourcesTree.setInput(profileElement); disposables.add(profileElement.onDidChange(e => { - if (e.flags || e.copyFrom) { + if (e.flags || e.copyFrom || e.copyFlags || e.disabled) { const viewState = this.resourcesTree.getViewState(); this.resourcesTree.setInput(profileElement, { ...viewState, @@ -677,34 +718,57 @@ class ProfileWidget extends Disposable { } })); - if (profileElement.primaryAction) { + const [primaryTitleButtons, secondatyTitleButtons] = profileElement.titleButtons; + if (primaryTitleButtons?.length || secondatyTitleButtons?.length) { this.buttonContainer.classList.remove('hide'); - const button = disposables.add(new Button(this.buttonContainer, { - supportIcons: true, - ...defaultButtonStyles - })); - button.label = profileElement.primaryAction.label; - button.enabled = profileElement.primaryAction.enabled; - disposables.add(button.onDidClick(() => this.editorProgressService.showWhile(profileElement.primaryAction!.run()))); - disposables.add(profileElement.primaryAction.onDidChange((e) => { - if (!isUndefined(e.enabled)) { - button.enabled = profileElement.primaryAction!.enabled; + + if (secondatyTitleButtons?.length) { + for (const action of secondatyTitleButtons) { + const button = disposables.add(new Button(this.buttonContainer, { + ...defaultButtonStyles, + secondary: true + })); + button.label = action.label; + button.enabled = action.enabled; + disposables.add(button.onDidClick(() => this.editorProgressService.showWhile(action.run()))); + disposables.add(action.onDidChange((e) => { + if (!isUndefined(e.enabled)) { + button.enabled = action.enabled; + } + })); } - })); - disposables.add(profileElement.onDidChange(e => { - if (e.message) { - button.setTitle(profileElement.message ?? profileElement.primaryAction!.label); - button.element.classList.toggle('error', !!profileElement.message); + } + + if (primaryTitleButtons?.length) { + for (const action of primaryTitleButtons) { + const button = disposables.add(new Button(this.buttonContainer, { + ...defaultButtonStyles + })); + button.label = action.label; + button.enabled = action.enabled; + disposables.add(button.onDidClick(() => this.editorProgressService.showWhile(action.run()))); + disposables.add(action.onDidChange((e) => { + if (!isUndefined(e.enabled)) { + button.enabled = action.enabled; + } + })); + disposables.add(profileElement.onDidChange(e => { + if (e.message) { + button.setTitle(profileElement.message ?? action.label); + button.element.classList.toggle('error', !!profileElement.message); + } + })); } - })); + } + } else { this.buttonContainer.classList.add('hide'); } this.toolbar.setActions(profileElement.titleActions[0].slice(0), profileElement.titleActions[1].slice(0)); - this.nameInput.focus(); if (profileElement instanceof NewProfileElement) { + this.nameInput.focus(); this.nameInput.select(); } } @@ -712,40 +776,71 @@ class ProfileWidget extends Disposable { private renderProfileElement(profileElement: AbstractUserDataProfileElement): void { this.profileTitle.textContent = profileElement.name; this.nameInput.value = profileElement.name; + this.nameInput.validate(); + if (profileElement.disabled) { + this.nameInput.disable(); + } else { + this.nameInput.enable(); + } if (profileElement.icon) { this.iconElement.className = ThemeIcon.asClassName(ThemeIcon.fromId(profileElement.icon)); } else { this.iconElement.className = ThemeIcon.asClassName(ThemeIcon.fromId(DEFAULT_ICON.id)); } if (profileElement instanceof NewProfileElement) { + this.contentsTreeHeader.classList.add('new-profile'); + this.inheritLabelElement.textContent = localize('options', "Options"); + this.useAsDefaultProfileContainer.classList.add('hide'); this.copyFromContainer.classList.remove('hide'); + this.copyFromOptions = this.getCopyFromOptions(); const id = profileElement.copyFrom instanceof URI ? profileElement.copyFrom.toString() : profileElement.copyFrom?.id; const index = id ? this.copyFromOptions.findIndex(option => option.id === id) : 0; if (index !== -1) { this.copyFromSelectBox.setOptions(this.copyFromOptions); - this.copyFromSelectBox.setEnabled(true); + this.copyFromSelectBox.setEnabled(!profileElement.previewProfile && !profileElement.disabled); this.copyFromSelectBox.select(index); } else { this.copyFromSelectBox.setOptions([{ text: basename(profileElement.copyFrom as URI) }]); this.copyFromSelectBox.setEnabled(false); } - } else { + } else if (profileElement instanceof UserDataProfileElement) { + this.contentsTreeHeader.classList.remove('new-profile'); + this.inheritLabelElement.textContent = profileElement.profile.isDefault ? '' : localize('default profile', "Use Default Profile"); + this.useAsDefaultProfileContainer.classList.remove('hide'); + this.useAsDefaultProfileCheckbox.checked = profileElement.isNewWindowProfile; this.copyFromContainer.classList.add('hide'); } } + + private getCopyFromOptions(): (ISelectOptionItem & { id?: string; source?: IUserDataProfile | URI })[] { + const separator = { text: '\u2500\u2500\u2500\u2500\u2500\u2500', isDisabled: true }; + const copyFromOptions: (ISelectOptionItem & { id?: string; source?: IUserDataProfile | URI })[] = []; + copyFromOptions.push({ text: localize('empty profile', "None") }); + if (this._templates.length) { + copyFromOptions.push({ ...separator, decoratorRight: localize('from templates', "Profile Templates") }); + for (const template of this._templates) { + copyFromOptions.push({ text: template.name, id: template.url, source: URI.parse(template.url) }); + } + } + copyFromOptions.push({ ...separator, decoratorRight: localize('from existing profiles', "Existing Profiles") }); + for (const profile of this.userDataProfilesService.profiles) { + copyFromOptions.push({ text: profile.name, id: profile.id, source: profile }); + } + return copyFromOptions; + } } interface ProfileResourceTreeElement { - element: ProfileResourceType | IProfileResourceChildTreeItem; + element: IProfileChildElement; root: AbstractUserDataProfileElement; } class ProfileResourceTreeElementDelegate implements IListVirtualDelegate { getTemplateId(element: ProfileResourceTreeElement) { - if (!isString(element.element)) { + if (!(element.element).resourceType) { return ProfileResourceChildTreeItemRenderer.TEMPLATE_ID; } if (element.root instanceof NewProfileElement) { @@ -754,7 +849,7 @@ class ProfileResourceTreeElementDelegate implements IListVirtualDelegateelement.element).resourceType) { + if ((element.element).resourceType !== ProfileResourceType.Extensions && (element.element).resourceType !== ProfileResourceType.Snippets) { return false; } if (element.root instanceof NewProfileElement) { - return element.root.copyFrom !== undefined; + const resourceType = (element.element).resourceType; + if (element.root.getFlag(resourceType)) { + return true; + } + if (!element.root.hasResource(resourceType)) { + return false; + } + if (element.root.copyFrom === undefined) { + return false; + } + if (!element.root.getCopyFlag(resourceType)) { + return false; + } } return true; } @@ -782,20 +889,14 @@ class ProfileResourceTreeDataSource implements IAsyncDataSource { if (element instanceof AbstractUserDataProfileElement) { - const resourceTypes = [ - ProfileResourceType.Settings, - ProfileResourceType.Keybindings, - ProfileResourceType.Snippets, - ProfileResourceType.Tasks, - ProfileResourceType.Extensions - ]; - return resourceTypes.map(resourceType => ({ element: resourceType, root: element })); + const children = await element.getChildren(); + return children.map(e => ({ element: e, root: element })); } - if (isString(element.element)) { - const progressRunner = this.editorProgressService.show(true); + if ((element.element).resourceType) { + const progressRunner = this.editorProgressService.show(true, 500); try { - const extensions = await element.root.getChildren(element.element); - return extensions.map(extension => ({ element: extension, root: element.root })); + const extensions = await element.root.getChildren((element.element).resourceType); + return extensions.map(e => ({ element: e, root: element.root })); } finally { progressRunner.done(); } @@ -807,12 +908,13 @@ class ProfileResourceTreeDataSource implements IAsyncDataSource, index: number, templateData: IExistingProfileResourceTemplateData, height: number | undefined): void { @@ -876,27 +995,22 @@ class ExistingProfileResourceTreeRenderer extends AbstractProfileResourceTreeRen if (!(root instanceof UserDataProfileElement)) { throw new Error('ExistingProfileResourceTreeRenderer can only render existing profile element'); } - if (!isString(element)) { - throw new Error('ExistingProfileResourceTreeRenderer can only render profile resource types'); + if (!isProfileResourceTypeElement(element)) { + throw new Error('Invalid profile resource element'); } - templateData.label.textContent = this.getResourceTypeTitle(element); + templateData.label.textContent = this.getResourceTypeTitle(element.resourceType); if (root instanceof UserDataProfileElement && root.profile.isDefault) { - templateData.checkbox.checked = true; - templateData.checkbox.disable(); - templateData.description.classList.add('hide'); + templateData.checkbox.domNode.removeAttribute('tabindex'); + templateData.checkbox.domNode.classList.add('hide'); } else { - templateData.checkbox.enable(); - const checked = !root.getFlag(element); - templateData.checkbox.checked = checked; - templateData.description.classList.toggle('hide', checked); - templateData.elementDisposables.add(templateData.checkbox.onChange(() => root.setFlag(element, !templateData.checkbox.checked))); - templateData.elementDisposables.add(root.onDidChange(e => { - if (e.flags) { - templateData.description.classList.toggle('hide', !root.getFlag(element)); - } - })); + templateData.checkbox.domNode.classList.remove('hide'); + templateData.checkbox.domNode.setAttribute('tabindex', '0'); + templateData.checkbox.checked = root.getFlag(element.resourceType); + templateData.elementDisposables.add(templateData.checkbox.onChange(() => root.setFlag(element.resourceType, templateData.checkbox.checked))); } + + templateData.actionBar.setActions(element.action ? [element.action] : []); } } @@ -920,11 +1034,7 @@ class NewProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer const labelContainer = append(container, $('.profile-resource-type-label-container')); const label = append(labelContainer, $('span.profile-resource-type-label')); const selectBox = this._register(this.instantiationService.createInstance(SelectBox, - [ - { text: localize('empty', "Empty") }, - { text: localize('copy', "Copy") }, - { text: localize('default', "Use Default Profile") } - ], + [], 0, this.contextViewService, defaultSelectBoxStyles, @@ -935,7 +1045,16 @@ class NewProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer const selectContainer = append(container, $('.profile-select-container')); selectBox.render(selectContainer); - return { label, selectContainer, selectBox, disposables, elementDisposables: disposables.add(new DisposableStore()) }; + const actionsContainer = append(container, $('.profile-tree-item-actions-container')); + const actionBar = disposables.add(this.instantiationService.createInstance(WorkbenchToolBar, + actionsContainer, + { + hoverDelegate: disposables.add(createInstantHoverDelegate()), + highlightToggledItems: true + } + )); + + return { label, selectContainer, selectBox, actionBar, disposables, elementDisposables: disposables.add(new DisposableStore()) }; } renderElement({ element: profileResourceTreeElement }: ITreeNode, index: number, templateData: INewProfileResourceTemplateData, height: number | undefined): void { @@ -944,15 +1063,34 @@ class NewProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer if (!(root instanceof NewProfileElement)) { throw new Error('NewProfileResourceTreeRenderer can only render new profile element'); } - if (!isString(element)) { - throw new Error('NewProfileResourceTreeRenderer can only profile resoyrce types'); + if (!isProfileResourceTypeElement(element)) { + throw new Error('Invalid profile resource element'); } - templateData.label.textContent = this.getResourceTypeTitle(element); - templateData.selectBox.select(root.getCopyFlag(element) ? 1 : root.getFlag(element) ? 2 : 0); - templateData.elementDisposables.add(templateData.selectBox.onDidSelect(option => { - root.setFlag(element, option.index === 2); - root.setCopyFlag(element, option.index === 1); - })); + templateData.label.textContent = this.getResourceTypeTitle(element.resourceType); + if (root.copyFrom && root.hasResource(element.resourceType)) { + const copyFromName = root.getCopyFromName(); + templateData.selectBox.setOptions([ + { text: localize('empty', "Empty") }, + { text: copyFromName ? localize('copy from', "Copy ({0})", copyFromName) : localize('copy', "Copy") }, + { text: localize('default', "Use Default Profile") } + ]); + templateData.selectBox.select(root.getCopyFlag(element.resourceType) ? 1 : root.getFlag(element.resourceType) ? 2 : 0); + templateData.elementDisposables.add(templateData.selectBox.onDidSelect(option => { + root.setFlag(element.resourceType, option.index === 2); + root.setCopyFlag(element.resourceType, option.index === 1); + })); + } else { + templateData.selectBox.setOptions([ + { text: localize('empty', "Empty") }, + { text: localize('default', "Use Default Profile") } + ]); + templateData.selectBox.select(root.getFlag(element.resourceType) ? 1 : 0); + templateData.elementDisposables.add(templateData.selectBox.onDidSelect(option => { + root.setFlag(element.resourceType, option.index === 1); + })); + } + templateData.selectBox.setEnabled(!root.disabled); + templateData.actionBar.setActions(element.action ? [element.action] : []); } } @@ -965,7 +1103,7 @@ class ProfileResourceChildTreeItemRenderer extends AbstractProfileResourceTreeRe private readonly hoverDelegate: IHoverDelegate; constructor( - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.labels = instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER); @@ -978,16 +1116,29 @@ class ProfileResourceChildTreeItemRenderer extends AbstractProfileResourceTreeRe const checkbox = disposables.add(new Checkbox('', false, defaultCheckboxStyles)); append(container, checkbox.domNode); const resourceLabel = disposables.add(this.labels.create(container, { hoverDelegate: this.hoverDelegate })); - return { checkbox, resourceLabel, disposables, elementDisposables: disposables.add(new DisposableStore()) }; + + const actionsContainer = append(container, $('.profile-tree-item-actions-container')); + const actionBar = disposables.add(this.instantiationService.createInstance(WorkbenchToolBar, + actionsContainer, + { + hoverDelegate: disposables.add(createInstantHoverDelegate()), + highlightToggledItems: true + } + )); + + return { checkbox, resourceLabel, actionBar, disposables, elementDisposables: disposables.add(new DisposableStore()) }; } renderElement({ element: profileResourceTreeElement }: ITreeNode, index: number, templateData: IProfileResourceChildTreeItemTemplateData, height: number | undefined): void { templateData.elementDisposables.clear(); const { element } = profileResourceTreeElement; - if (isString(element)) { - throw new Error('NewProfileResourceTreeRenderer can only render profile resource child tree items'); + + if (!isProfileResourceChildElement(element)) { + throw new Error('Invalid profile resource element'); } + if (element.checkbox) { + templateData.checkbox.domNode.setAttribute('tabindex', '0'); templateData.checkbox.domNode.classList.remove('hide'); templateData.checkbox.checked = element.checkbox.isChecked; templateData.checkbox.domNode.ariaLabel = element.checkbox.accessibilityInformation?.label ?? ''; @@ -995,20 +1146,21 @@ class ProfileResourceChildTreeItemRenderer extends AbstractProfileResourceTreeRe templateData.checkbox.domNode.role = element.checkbox.accessibilityInformation.role; } } else { + templateData.checkbox.domNode.removeAttribute('tabindex'); templateData.checkbox.domNode.classList.add('hide'); } - const resource = URI.revive(element.resourceUri); templateData.resourceLabel.setResource( { - name: resource ? basename(resource) : element.label?.label, - description: isString(element.description) ? element.description : undefined, - resource + name: element.resource ? basename(element.resource) : element.label, + resource: element.resource }, { forceLabel: true, - hideIcon: !resource, + icon: element.icon, + hideIcon: !element.resource && !element.icon, }); + templateData.actionBar.setActions(element.action ? [element.action] : []); } } diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts index bde549ecefa..237d9142b36 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Action, IAction, Separator } from 'vs/base/common/actions'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -25,7 +25,15 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { IFileService } from 'vs/platform/files/common/files'; import { generateUuid } from 'vs/base/common/uuid'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { ITreeItemCheckboxState } from 'vs/workbench/common/views'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CONFIG_NEW_WINDOW_PROFILE } from 'vs/workbench/common/configuration'; export type ChangeEvent = { readonly name?: boolean; @@ -35,15 +43,33 @@ export type ChangeEvent = { readonly message?: boolean; readonly copyFrom?: boolean; readonly copyFlags?: boolean; + readonly preview?: boolean; + readonly disabled?: boolean; + readonly newWindowProfile?: boolean; }; -export interface IProfileElement { - readonly onDidChange?: Event; - readonly name: string; - readonly icon?: string; - readonly flags?: UseDefaultProfileFlags; - readonly active?: boolean; - readonly message?: string; +export interface IProfileChildElement { + readonly handle: string; + readonly action?: IAction; + readonly checkbox?: ITreeItemCheckboxState; +} + +export interface IProfileResourceTypeElement extends IProfileChildElement { + readonly resourceType: ProfileResourceType; +} + +export interface IProfileResourceTypeChildElement extends IProfileChildElement { + readonly label: string; + readonly resource?: URI; + readonly icon?: ThemeIcon; +} + +export function isProfileResourceTypeElement(element: IProfileChildElement): element is IProfileResourceTypeElement { + return (element as IProfileResourceTypeElement).resourceType !== undefined; +} + +export function isProfileResourceChildElement(element: IProfileChildElement): element is IProfileResourceTypeChildElement { + return (element as IProfileResourceTypeChildElement).label !== undefined; } export abstract class AbstractUserDataProfileElement extends Disposable { @@ -51,12 +77,16 @@ export abstract class AbstractUserDataProfileElement extends Disposable { protected readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; + private readonly saveScheduler = this._register(new RunOnceScheduler(() => this.doSave(), 500)); + constructor( name: string, icon: string | undefined, flags: UseDefaultProfileFlags | undefined, isActive: boolean, + @IUserDataProfileManagementService protected readonly userDataProfileManagementService: IUserDataProfileManagementService, @IUserDataProfilesService protected readonly userDataProfilesService: IUserDataProfilesService, + @ICommandService protected readonly commandService: ICommandService, @IInstantiationService protected readonly instantiationService: IInstantiationService, ) { super(); @@ -68,17 +98,16 @@ export abstract class AbstractUserDataProfileElement extends Disposable { if (!e.message) { this.validate(); } - if (this.primaryAction) { - this.primaryAction.enabled = !this.message; - } + this.save(); })); } private _name = ''; get name(): string { return this._name; } - set name(label: string) { - if (this._name !== label) { - this._name = label; + set name(name: string) { + name = name.trim(); + if (this._name !== name) { + this._name = name; this._onDidChange.fire({ name: true }); } } @@ -119,6 +148,15 @@ export abstract class AbstractUserDataProfileElement extends Disposable { } } + private _disabled: boolean = false; + get disabled(): boolean { return this._disabled; } + set disabled(saving: boolean) { + if (this._disabled !== saving) { + this._disabled = saving; + this._onDidChange.fire({ disabled: true }); + } + } + getFlag(key: ProfileResourceType): boolean { return this.flags?.[key] ?? false; } @@ -151,95 +189,104 @@ export abstract class AbstractUserDataProfileElement extends Disposable { this.message = undefined; } - async getChildren(resourceType: ProfileResourceType): Promise { + async getChildren(resourceType?: ProfileResourceType): Promise { + if (resourceType === undefined) { + const resourceTypes = [ + ProfileResourceType.Settings, + ProfileResourceType.Keybindings, + ProfileResourceType.Tasks, + ProfileResourceType.Snippets, + ProfileResourceType.Extensions + ]; + return Promise.all(resourceTypes.map>(async r => { + const children = (r === ProfileResourceType.Settings + || r === ProfileResourceType.Keybindings + || r === ProfileResourceType.Tasks) ? await this.getChildrenForResourceType(r) : []; + return { + handle: r, + checkbox: undefined, + resourceType: r, + action: children.length + ? new Action('_open', + localize('open', "Open to the Side"), + ThemeIcon.asClassName(Codicon.goToFile), + true, + () => children[0]?.action?.run()) + : undefined + }; + })); + } + return this.getChildrenForResourceType(resourceType); + } + + protected async getChildrenForResourceType(resourceType: ProfileResourceType): Promise { return []; } - protected async getChildrenFromProfile(profile: IUserDataProfile, resourceType: ProfileResourceType): Promise { + protected async getChildrenFromProfile(profile: IUserDataProfile, resourceType: ProfileResourceType): Promise { profile = this.getFlag(resourceType) ? this.userDataProfilesService.defaultProfile : profile; + let children: IProfileResourceChildTreeItem[] = []; switch (resourceType) { case ProfileResourceType.Settings: - return this.instantiationService.createInstance(SettingsResourceTreeItem, profile).getChildren(); + children = await this.instantiationService.createInstance(SettingsResourceTreeItem, profile).getChildren(); + break; case ProfileResourceType.Keybindings: - return this.instantiationService.createInstance(KeybindingsResourceTreeItem, profile).getChildren(); + children = await this.instantiationService.createInstance(KeybindingsResourceTreeItem, profile).getChildren(); + break; case ProfileResourceType.Snippets: - return (await this.instantiationService.createInstance(SnippetsResourceTreeItem, profile).getChildren()) ?? []; + children = (await this.instantiationService.createInstance(SnippetsResourceTreeItem, profile).getChildren()) ?? []; + break; case ProfileResourceType.Tasks: - return this.instantiationService.createInstance(TasksResourceTreeItem, profile).getChildren(); + children = await this.instantiationService.createInstance(TasksResourceTreeItem, profile).getChildren(); + break; case ProfileResourceType.Extensions: - return this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, profile).getChildren(); + children = await this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, profile).getChildren(); + break; } - return []; + return children.map(child => this.toUserDataProfileResourceChildElement(child)); } - protected getInitialName(): string { + protected toUserDataProfileResourceChildElement(child: IProfileResourceChildTreeItem): IProfileResourceTypeChildElement { + return { + handle: child.handle, + checkbox: child.checkbox, + label: child.label?.label ?? '', + resource: URI.revive(child.resourceUri), + icon: child.themeIcon, + action: new Action('_openChild', localize('open', "Open to the Side"), ThemeIcon.asClassName(Codicon.goToFile), true, async () => { + if (child.parent.type === ProfileResourceType.Extensions) { + await this.commandService.executeCommand('extension.open', child.handle, undefined, true, undefined, true); + } else if (child.resourceUri) { + await this.commandService.executeCommand(API_OPEN_EDITOR_COMMAND_ID, child.resourceUri, [SIDE_GROUP], undefined); + } + }) + }; + + } + + getInitialName(): string { return ''; } - abstract readonly primaryAction?: Action; - abstract readonly titleActions: [IAction[], IAction[]]; - abstract readonly contextMenuActions: IAction[]; -} - -export class UserDataProfileElement extends AbstractUserDataProfileElement implements IProfileElement { - - get profile(): IUserDataProfile { return this._profile; } - - readonly primaryAction = undefined; - - private readonly saveScheduler = this._register(new RunOnceScheduler(() => this.doSave(), 500)); - - constructor( - private _profile: IUserDataProfile, - readonly titleActions: [IAction[], IAction[]], - readonly contextMenuActions: IAction[], - @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, - @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super( - _profile.name, - _profile.icon, - _profile.useDefaultFlags, - userDataProfileService.currentProfile.id === _profile.id, - userDataProfilesService, - instantiationService, - ); - this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => this.active = this.userDataProfileService.currentProfile.id === this.profile.id)); - this._register(this.userDataProfilesService.onDidChangeProfiles(() => { - const profile = this.userDataProfilesService.profiles.find(p => p.id === this.profile.id); - if (profile) { - this._profile = profile; - this.name = profile.name; - this.icon = profile.icon; - this.flags = profile.useDefaultFlags; - } - })); - this._register(this.onDidChange(e => { - this.save(); - })); - } - - private hasUnsavedChanges(): boolean { - if (this.name !== this.profile.name) { - return true; - } - if (this.icon !== this.profile.icon) { - return true; - } - if (!equals(this.flags ?? {}, this.profile.useDefaultFlags ?? {})) { - return true; - } - return false; - } - save(): void { this.saveScheduler.schedule(); } - private async doSave(): Promise { - if (!this.hasUnsavedChanges()) { + private hasUnsavedChanges(profile: IUserDataProfile): boolean { + if (this.name !== profile.name) { + return true; + } + if (this.icon !== profile.icon) { + return true; + } + if (!equals(this.flags ?? {}, profile.useDefaultFlags ?? {})) { + return true; + } + return false; + } + + protected async saveProfile(profile: IUserDataProfile): Promise { + if (!this.hasUnsavedChanges(profile)) { return; } this.validate(); @@ -250,18 +297,91 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple ? this.flags.settings && this.flags.keybindings && this.flags.tasks && this.flags.globalState && this.flags.extensions ? undefined : this.flags : undefined; - await this.userDataProfileManagementService.updateProfile(this.profile, { + return await this.userDataProfileManagementService.updateProfile(profile, { name: this.name, icon: this.icon, - useDefaultFlags: this.profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags + useDefaultFlags: profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags }); } - override async getChildren(resourceType: ProfileResourceType): Promise { + abstract readonly titleButtons: [Action[], Action[]]; + abstract readonly titleActions: [IAction[], IAction[]]; + abstract readonly actions: [IAction[], IAction[]]; + + protected abstract doSave(): Promise; +} + +export class UserDataProfileElement extends AbstractUserDataProfileElement { + + get profile(): IUserDataProfile { return this._profile; } + + constructor( + private _profile: IUserDataProfile, + readonly titleButtons: [Action[], Action[]], + readonly titleActions: [IAction[], IAction[]], + readonly actions: [IAction[], IAction[]], + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IUserDataProfileManagementService userDataProfileManagementService: IUserDataProfileManagementService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @ICommandService commandService: ICommandService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super( + _profile.name, + _profile.icon, + _profile.useDefaultFlags, + userDataProfileService.currentProfile.id === _profile.id, + userDataProfileManagementService, + userDataProfilesService, + commandService, + instantiationService, + ); + this._isNewWindowProfile = this.configurationService.getValue(CONFIG_NEW_WINDOW_PROFILE) === this.profile.name; + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(CONFIG_NEW_WINDOW_PROFILE)) { + this.isNewWindowProfile = this.configurationService.getValue(CONFIG_NEW_WINDOW_PROFILE) === this.profile.name; + } + } + )); + this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => this.active = this.userDataProfileService.currentProfile.id === this.profile.id)); + this._register(this.userDataProfilesService.onDidChangeProfiles(() => { + const profile = this.userDataProfilesService.profiles.find(p => p.id === this.profile.id); + if (profile) { + this._profile = profile; + this.name = profile.name; + this.icon = profile.icon; + this.flags = profile.useDefaultFlags; + } + })); + } + + public async toggleNewWindowProfile(): Promise { + if (this._isNewWindowProfile) { + await this.configurationService.updateValue(CONFIG_NEW_WINDOW_PROFILE, null); + } else { + await this.configurationService.updateValue(CONFIG_NEW_WINDOW_PROFILE, this.profile.name); + } + } + + private _isNewWindowProfile: boolean = false; + get isNewWindowProfile(): boolean { return this._isNewWindowProfile; } + set isNewWindowProfile(isNewWindowProfile: boolean) { + if (this._isNewWindowProfile !== isNewWindowProfile) { + this._isNewWindowProfile = isNewWindowProfile; + this._onDidChange.fire({ newWindowProfile: true }); + } + } + + protected override async doSave(): Promise { + await this.saveProfile(this.profile); + } + + protected override async getChildrenForResourceType(resourceType: ProfileResourceType): Promise { return this.getChildrenFromProfile(this.profile, resourceType); } - protected override getInitialName(): string { + override getInitialName(): string { return this.profile.name; } @@ -269,17 +389,25 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple const USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME = 'userdataprofiletemplatepreview'; -export class NewProfileElement extends AbstractUserDataProfileElement implements IProfileElement { +export class NewProfileElement extends AbstractUserDataProfileElement { + + private templatePromise: CancelablePromise | undefined; + private template: IUserDataProfileTemplate | null = null; + + private defaultName: string; + private defaultIcon: string | undefined; constructor( name: string, copyFrom: URI | IUserDataProfile | undefined, - readonly primaryAction: Action, + readonly titleButtons: [Action[], Action[]], readonly titleActions: [IAction[], IAction[]], - readonly contextMenuActions: Action[], + readonly actions: [IAction[], IAction[]], @IFileService private readonly fileService: IFileService, @IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService, + @IUserDataProfileManagementService userDataProfileManagementService: IUserDataProfileManagementService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @ICommandService commandService: ICommandService, @IInstantiationService instantiationService: IInstantiationService, ) { super( @@ -287,11 +415,15 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements undefined, undefined, false, + userDataProfileManagementService, userDataProfilesService, + commandService, instantiationService, ); + this.defaultName = name; this._copyFrom = copyFrom; this._copyFlags = this.getCopyFlagsFrom(copyFrom); + this.initialize(); this._register(this.fileService.registerProvider(USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME, this._register(new InMemoryFileSystemProvider()))); } @@ -303,6 +435,11 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements this._onDidChange.fire({ copyFrom: true }); this.flags = undefined; this.copyFlags = this.getCopyFlagsFrom(copyFrom); + if (copyFrom instanceof URI) { + this.templatePromise?.cancel(); + this.templatePromise = undefined; + } + this.initialize(); } } @@ -315,6 +452,15 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements } } + private _previewProfile: IUserDataProfile | undefined; + get previewProfile(): IUserDataProfile | undefined { return this._previewProfile; } + set previewProfile(profile: IUserDataProfile | undefined) { + if (this._previewProfile !== profile) { + this._previewProfile = profile; + this._onDidChange.fire({ preview: true }); + } + } + private getCopyFlagsFrom(copyFrom: URI | IUserDataProfile | undefined): ProfileResourceTypeFlags | undefined { return copyFrom ? { settings: true, @@ -325,6 +471,89 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements } : undefined; } + private async initialize(): Promise { + this.disabled = true; + try { + if (this.copyFrom instanceof URI) { + await this.resolveTemplate(this.copyFrom); + if (this.template) { + if (this.defaultName === this.name) { + this.name = this.defaultName = this.template.name ?? ''; + } + if (this.defaultIcon === this.icon) { + this.icon = this.defaultIcon = this.template.icon; + } + this.setCopyFlag(ProfileResourceType.Settings, !!this.template.settings); + this.setCopyFlag(ProfileResourceType.Keybindings, !!this.template.keybindings); + this.setCopyFlag(ProfileResourceType.Tasks, !!this.template.tasks); + this.setCopyFlag(ProfileResourceType.Snippets, !!this.template.snippets); + this.setCopyFlag(ProfileResourceType.Extensions, !!this.template.extensions); + } + return; + } + + if (isUserDataProfile(this.copyFrom)) { + if (this.defaultName === this.name) { + this.name = this.defaultName = localize('copy from', "{0} (Copy)", this.copyFrom.name); + } + if (this.defaultIcon === this.icon) { + this.icon = this.defaultIcon = this.copyFrom.icon; + } + this.setCopyFlag(ProfileResourceType.Settings, true); + this.setCopyFlag(ProfileResourceType.Keybindings, true); + this.setCopyFlag(ProfileResourceType.Tasks, true); + this.setCopyFlag(ProfileResourceType.Snippets, true); + this.setCopyFlag(ProfileResourceType.Extensions, true); + return; + } + + if (this.defaultName === this.name) { + this.name = this.defaultName = localize('untitled', "Untitled"); + } + if (this.defaultIcon === this.icon) { + this.icon = this.defaultIcon = undefined; + } + this.setCopyFlag(ProfileResourceType.Settings, false); + this.setCopyFlag(ProfileResourceType.Keybindings, false); + this.setCopyFlag(ProfileResourceType.Tasks, false); + this.setCopyFlag(ProfileResourceType.Snippets, false); + this.setCopyFlag(ProfileResourceType.Extensions, false); + } finally { + this.disabled = false; + } + } + + async resolveTemplate(uri: URI): Promise { + if (!this.templatePromise) { + this.templatePromise = createCancelablePromise(async token => { + const template = await this.userDataProfileImportExportService.resolveProfileTemplate(uri); + if (!token.isCancellationRequested) { + this.template = template; + } + }); + } + await this.templatePromise; + return this.template; + } + + hasResource(resourceType: ProfileResourceType): boolean { + if (this.template) { + switch (resourceType) { + case ProfileResourceType.Settings: + return !!this.template.settings; + case ProfileResourceType.Keybindings: + return !!this.template.keybindings; + case ProfileResourceType.Snippets: + return !!this.template.snippets; + case ProfileResourceType.Tasks: + return !!this.template.tasks; + case ProfileResourceType.Extensions: + return !!this.template.extensions; + } + } + return true; + } + getCopyFlag(key: ProfileResourceType): boolean { return this.copyFlags?.[key] ?? false; } @@ -335,56 +564,85 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements this.copyFlags = flags; } - override async getChildren(resourceType: ProfileResourceType): Promise { + getCopyFromName(): string | undefined { + if (isUserDataProfile(this.copyFrom)) { + return this.copyFrom.name; + } + if (this.template) { + return this.template.name; + } + return undefined; + } + + protected override async getChildrenForResourceType(resourceType: ProfileResourceType): Promise { + if (this.getFlag(resourceType)) { + return this.getChildrenFromProfile(this.userDataProfilesService.defaultProfile, resourceType); + } if (!this.getCopyFlag(resourceType)) { return []; } if (this.copyFrom instanceof URI) { - const template = await this.userDataProfileImportExportService.resolveProfileTemplate(this.copyFrom); - if (!template) { + await this.resolveTemplate(this.copyFrom); + if (!this.template) { return []; } - return this.getChildrenFromProfileTemplate(template, resourceType); + return this.getChildrenFromProfileTemplate(this.template, resourceType); } if (this.copyFrom) { return this.getChildrenFromProfile(this.copyFrom, resourceType); } - if (this.getFlag(resourceType)) { - return this.getChildrenFromProfile(this.userDataProfilesService.defaultProfile, resourceType); - } return []; } - private async getChildrenFromProfileTemplate(profileTemplate: IUserDataProfileTemplate, resourceType: ProfileResourceType): Promise { + private async getChildrenFromProfileTemplate(profileTemplate: IUserDataProfileTemplate, resourceType: ProfileResourceType): Promise { const profile = toUserDataProfile(generateUuid(), this.name, URI.file('/root').with({ scheme: USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME }), URI.file('/cache').with({ scheme: USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME })); switch (resourceType) { case ProfileResourceType.Settings: if (profileTemplate.settings) { await this.instantiationService.createInstance(SettingsResource).apply(profileTemplate.settings, profile); + return this.getChildrenFromProfile(profile, resourceType); } - return this.getChildrenFromProfile(profile, resourceType); + return []; case ProfileResourceType.Keybindings: if (profileTemplate.keybindings) { await this.instantiationService.createInstance(KeybindingsResource).apply(profileTemplate.keybindings, profile); + return this.getChildrenFromProfile(profile, resourceType); } - return this.getChildrenFromProfile(profile, resourceType); + return []; case ProfileResourceType.Snippets: if (profileTemplate.snippets) { await this.instantiationService.createInstance(SnippetsResource).apply(profileTemplate.snippets, profile); + return this.getChildrenFromProfile(profile, resourceType); } - return this.getChildrenFromProfile(profile, resourceType); + return []; case ProfileResourceType.Tasks: if (profileTemplate.tasks) { await this.instantiationService.createInstance(TasksResource).apply(profileTemplate.tasks, profile); + return this.getChildrenFromProfile(profile, resourceType); } - return this.getChildrenFromProfile(profile, resourceType); + return []; case ProfileResourceType.Extensions: if (profileTemplate.extensions) { - return this.instantiationService.createInstance(ExtensionsResourceImportTreeItem, profileTemplate.extensions).getChildren(); + const children = await this.instantiationService.createInstance(ExtensionsResourceImportTreeItem, profileTemplate.extensions).getChildren(); + return children.map(child => this.toUserDataProfileResourceChildElement(child)); } + return []; } return []; } + + override getInitialName(): string { + return this.previewProfile?.name ?? ''; + } + + protected override async doSave(): Promise { + if (this.previewProfile) { + const profile = await this.saveProfile(this.previewProfile); + if (profile) { + this.previewProfile = profile; + } + } + } } export class UserDataProfilesEditorModel extends EditorModel { @@ -430,53 +688,107 @@ export class UserDataProfilesEditorModel extends EditorModel { @IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService, @IDialogService private readonly dialogService: IDialogService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IHostService private readonly hostService: IHostService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); for (const profile of userDataProfilesService.profiles) { - this._profiles.push(this.createProfileElement(profile)); + if (!profile.isTransient) { + this._profiles.push(this.createProfileElement(profile)); + } } this._register(toDisposable(() => this._profiles.splice(0, this._profiles.length).map(([, disposables]) => disposables.dispose()))); this._register(userDataProfilesService.onDidChangeProfiles(e => this.onDidChangeProfiles(e))); } private onDidChangeProfiles(e: DidChangeProfilesEvent): void { + let changed = false; for (const profile of e.added) { - if (profile.name !== this.newProfileElement?.name) { + if (!profile.isTransient && profile.name !== this.newProfileElement?.name) { + changed = true; this._profiles.push(this.createProfileElement(profile)); } } for (const profile of e.removed) { + if (profile.id === this.newProfileElement?.previewProfile?.id) { + this.newProfileElement.previewProfile = undefined; + } const index = this._profiles.findIndex(([p]) => p instanceof UserDataProfileElement && p.profile.id === profile.id); if (index !== -1) { + changed = true; this._profiles.splice(index, 1).map(([, disposables]) => disposables.dispose()); } } - this._onDidChange.fire(undefined); + if (changed) { + this._onDidChange.fire(undefined); + } } private createProfileElement(profile: IUserDataProfile): [UserDataProfileElement, DisposableStore] { const disposables = new DisposableStore(); - const activateAction = disposables.add(new Action('userDataProfile.activate', localize('active', "Activate"), ThemeIcon.asClassName(Codicon.check), true, () => this.userDataProfileManagementService.switchProfile(profile))); - activateAction.checked = this.userDataProfileService.currentProfile.id === profile.id; - disposables.add(this.userDataProfileService.onDidChangeCurrentProfile(() => activateAction.checked = this.userDataProfileService.currentProfile.id === profile.id)); - const copyFromProfileAction = disposables.add(new Action('userDataProfile.copyFromProfile', localize('copyFromProfile', "Save As..."), ThemeIcon.asClassName(Codicon.copy), true, () => this.createNewProfile(profile))); - const exportAction = disposables.add(new Action('userDataProfile.export', localize('export', "Export..."), ThemeIcon.asClassName(Codicon.export), true, () => this.exportProfile(profile))); - const deleteAction = disposables.add(new Action('userDataProfile.delete', localize('delete', "Delete"), ThemeIcon.asClassName(Codicon.trash), true, () => this.removeProfile(profile))); + const activateAction = disposables.add(new Action( + 'userDataProfile.activate', + localize('active', "Use for Current Window"), + ThemeIcon.asClassName(Codicon.check), + true, + () => this.userDataProfileManagementService.switchProfile(profileElement.profile) + )); + + const copyFromProfileAction = disposables.add(new Action( + 'userDataProfile.copyFromProfile', + localize('copyFromProfile', "Duplicate..."), + ThemeIcon.asClassName(Codicon.copy), + true, () => this.createNewProfile(profileElement.profile) + )); + + const exportAction = disposables.add(new Action( + 'userDataProfile.export', + localize('export', "Export..."), + ThemeIcon.asClassName(Codicon.export), + true, + () => this.exportProfile(profileElement.profile) + )); + + const deleteAction = disposables.add(new Action( + 'userDataProfile.delete', + localize('delete', "Delete"), + ThemeIcon.asClassName(Codicon.trash), + true, + () => this.removeProfile(profileElement.profile) + )); + + const newWindowAction = disposables.add(new Action( + 'userDataProfile.newWindow', + localize('open new window', "Open New Window with this Profile"), + ThemeIcon.asClassName(Codicon.emptyWindow), + true, + () => this.openWindow(profileElement.profile) + )); + + const useAsNewWindowProfileAction = disposables.add(new Action( + 'userDataProfile.useAsNewWindowProfile', + localize('use as new window', "Use for New Windows"), + undefined, + true, + () => profileElement.toggleNewWindowProfile() + )); const titlePrimaryActions: IAction[] = []; - titlePrimaryActions.push(activateAction); - titlePrimaryActions.push(exportAction); - if (!profile.isDefault) { - titlePrimaryActions.push(deleteAction); - } - + titlePrimaryActions.push(newWindowAction); const titleSecondaryActions: IAction[] = []; titleSecondaryActions.push(copyFromProfileAction); + titleSecondaryActions.push(exportAction); + if (!profile.isDefault) { + titleSecondaryActions.push(new Separator()); + titleSecondaryActions.push(deleteAction); + } + const primaryActions: IAction[] = []; + primaryActions.push(newWindowAction); const secondaryActions: IAction[] = []; secondaryActions.push(activateAction); + secondaryActions.push(useAsNewWindowProfileAction); secondaryActions.push(new Separator()); secondaryActions.push(copyFromProfileAction); secondaryActions.push(exportAction); @@ -484,28 +796,81 @@ export class UserDataProfilesEditorModel extends EditorModel { secondaryActions.push(new Separator()); secondaryActions.push(deleteAction); } + const profileElement = disposables.add(this.instantiationService.createInstance(UserDataProfileElement, profile, + [[], []], [titlePrimaryActions, titleSecondaryActions], - secondaryActions, + [primaryActions, secondaryActions] )); + + activateAction.checked = this.userDataProfileService.currentProfile.id === profileElement.profile.id; + disposables.add(this.userDataProfileService.onDidChangeCurrentProfile(() => + activateAction.checked = this.userDataProfileService.currentProfile.id === profileElement.profile.id)); + + useAsNewWindowProfileAction.checked = profileElement.isNewWindowProfile; + disposables.add(profileElement.onDidChange(e => { + if (e.newWindowProfile) { + useAsNewWindowProfileAction.checked = profileElement.isNewWindowProfile; + } + })); + return [profileElement, disposables]; } - createNewProfile(copyFrom?: URI | IUserDataProfile): IProfileElement { + async createNewProfile(copyFrom?: URI | IUserDataProfile): Promise { + if (this.newProfileElement) { + const result = await this.dialogService.confirm({ + type: 'info', + message: localize('new profile exists', "A new profile is already being created. Do you want to discard it and create a new one?"), + primaryButton: localize('discard', "Discard & Create"), + cancelButton: localize('cancel', "Cancel") + }); + if (!result.confirmed) { + return; + } + this.revert(); + } if (!this.newProfileElement) { const disposables = new DisposableStore(); - const discardAction = disposables.add(new Action('userDataProfile.discard', localize('discard', "Discard"), ThemeIcon.asClassName(Codicon.close), true, () => { - this.removeNewProfile(); - this._onDidChange.fire(undefined); - })); - this.newProfileElement = disposables.add(this.instantiationService.createInstance(NewProfileElement, - localize('untitled', "Untitled"), - copyFrom, - disposables.add(new Action('userDataProfile.create', localize('create', "Create & Apply"), undefined, true, () => this.saveNewProfile())), - [[discardAction], []], - [discardAction], + const cancellationTokenSource = new CancellationTokenSource(); + disposables.add(toDisposable(() => cancellationTokenSource.dispose(true))); + const createAction = disposables.add(new Action( + 'userDataProfile.create', + localize('create', "Create"), + undefined, + true, + () => this.saveNewProfile(false, cancellationTokenSource.token) )); + const cancelAction = disposables.add(new Action( + 'userDataProfile.cancel', + localize('cancel', "Cancel"), + ThemeIcon.asClassName(Codicon.trash), + true, + () => this.discardNewProfile() + )); + const previewProfileAction = disposables.add(new Action( + 'userDataProfile.preview', + localize('preview', "Preview"), + ThemeIcon.asClassName(Codicon.openPreview), + true, + () => this.previewNewProfile(cancellationTokenSource.token) + )); + this.newProfileElement = disposables.add(this.instantiationService.createInstance(NewProfileElement, + copyFrom ? '' : localize('untitled', "Untitled"), + copyFrom, + [[createAction], [cancelAction, previewProfileAction]], + [[], []], + [[cancelAction], []], + )); + disposables.add(this.newProfileElement.onDidChange(e => { + if (e.preview) { + previewProfileAction.checked = !!this.newProfileElement?.previewProfile; + } + if (e.disabled || e.message) { + previewProfileAction.enabled = createAction.enabled = !this.newProfileElement?.disabled && !this.newProfileElement?.message; + } + })); this._profiles.push([this.newProfileElement, disposables]); this._onDidChange.fire(this.newProfileElement); } @@ -527,45 +892,123 @@ export class UserDataProfilesEditorModel extends EditorModel { } } - async saveNewProfile(): Promise { + private async previewNewProfile(token: CancellationToken): Promise { if (!this.newProfileElement) { return; } - this.newProfileElement.validate(); - if (this.newProfileElement.message) { + if (this.newProfileElement.previewProfile) { return; } - const { flags, icon, name, copyFrom } = this.newProfileElement; - const useDefaultFlags: UseDefaultProfileFlags | undefined = flags - ? flags.settings && flags.keybindings && flags.tasks && flags.globalState && flags.extensions ? undefined : flags - : undefined; + const profile = await this.saveNewProfile(true, token); + if (profile) { + this.newProfileElement.previewProfile = profile; + await this.openWindow(profile); + } + } - type CreateProfileInfoClassification = { - owner: 'sandy081'; - comment: 'Report when profile is about to be created'; - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Type of profile source' }; - }; - type CreateProfileInfoEvent = { - source: string | undefined; - }; - const createProfileTelemetryData: CreateProfileInfoEvent = { source: copyFrom instanceof URI ? 'template' : isUserDataProfile(copyFrom) ? 'profile' : copyFrom ? 'external' : undefined }; - - if (copyFrom instanceof URI) { - this.telemetryService.publicLog2('userDataProfile.createFromTemplate', createProfileTelemetryData); - await this.userDataProfileImportExportService.importProfile(copyFrom, { mode: 'apply', name: name, useDefaultFlags, icon: icon ? icon : undefined, resourceTypeFlags: this.newProfileElement.copyFlags }); - } else if (isUserDataProfile(copyFrom)) { - this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); - await this.userDataProfileImportExportService.createFromProfile(copyFrom, name, { useDefaultFlags, icon: icon ? icon : undefined, resourceTypeFlags: this.newProfileElement.copyFlags }); - } else { - this.telemetryService.publicLog2('userDataProfile.createEmptyProfile', createProfileTelemetryData); - await this.userDataProfileManagementService.createAndEnterProfile(name, { useDefaultFlags, icon: icon ? icon : undefined }); + async saveNewProfile(transient?: boolean, token?: CancellationToken): Promise { + if (!this.newProfileElement) { + return undefined; } - this.removeNewProfile(); - const profile = this.userDataProfilesService.profiles.find(p => p.name === name); - if (profile) { + this.newProfileElement.validate(); + if (this.newProfileElement.message) { + return undefined; + } + + this.newProfileElement.disabled = true; + let profile: IUserDataProfile | undefined; + + try { + if (this.newProfileElement.previewProfile) { + if (!transient) { + profile = await this.userDataProfileManagementService.updateProfile(this.newProfileElement.previewProfile, { transient: false }); + } + } + else { + const { flags, icon, name, copyFrom } = this.newProfileElement; + const useDefaultFlags: UseDefaultProfileFlags | undefined = flags + ? flags.settings && flags.keybindings && flags.tasks && flags.globalState && flags.extensions ? undefined : flags + : undefined; + + type CreateProfileInfoClassification = { + owner: 'sandy081'; + comment: 'Report when profile is about to be created'; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Type of profile source' }; + }; + type CreateProfileInfoEvent = { + source: string | undefined; + }; + const createProfileTelemetryData: CreateProfileInfoEvent = { source: copyFrom instanceof URI ? 'template' : isUserDataProfile(copyFrom) ? 'profile' : copyFrom ? 'external' : undefined }; + + if (copyFrom instanceof URI) { + const template = await this.newProfileElement.resolveTemplate(copyFrom); + if (template) { + this.telemetryService.publicLog2('userDataProfile.createFromTemplate', createProfileTelemetryData); + profile = await this.userDataProfileImportExportService.createProfileFromTemplate( + template, + { + name, + useDefaultFlags, + icon, + resourceTypeFlags: this.newProfileElement.copyFlags, + transient + }, + token ?? CancellationToken.None + ); + } + } else if (isUserDataProfile(copyFrom)) { + this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); + profile = await this.userDataProfileImportExportService.createFromProfile( + copyFrom, + { + name, + useDefaultFlags, + icon: icon, + resourceTypeFlags: this.newProfileElement.copyFlags, + transient + }, + token ?? CancellationToken.None + ); + } else { + this.telemetryService.publicLog2('userDataProfile.createEmptyProfile', createProfileTelemetryData); + profile = await this.userDataProfileManagementService.createProfile(name, { useDefaultFlags, icon, transient }); + } + } + } finally { + if (this.newProfileElement) { + this.newProfileElement.disabled = false; + } + } + + if (token?.isCancellationRequested) { + if (profile) { + try { + await this.userDataProfileManagementService.removeProfile(profile); + } catch (error) { + // ignore + } + } + return; + } + + if (profile && !profile.isTransient && this.newProfileElement) { + this.removeNewProfile(); this.onDidChangeProfiles({ added: [profile], removed: [], updated: [], all: this.userDataProfilesService.profiles }); } + + return profile; + } + + private async discardNewProfile(): Promise { + if (!this.newProfileElement) { + return; + } + if (this.newProfileElement.previewProfile) { + await this.userDataProfileManagementService.removeProfile(this.newProfileElement.previewProfile); + } + this.removeNewProfile(); + this._onDidChange.fire(undefined); } private async removeProfile(profile: IUserDataProfile): Promise { @@ -580,7 +1023,11 @@ export class UserDataProfilesEditorModel extends EditorModel { } } + private async openWindow(profile: IUserDataProfile): Promise { + await this.hostService.openWindow({ forceProfile: profile.name }); + } + private async exportProfile(profile: IUserDataProfile): Promise { - return this.userDataProfileImportExportService.exportProfile2(profile); + return this.userDataProfileImportExportService.exportProfile(profile); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index c2de392a001..60678809819 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -50,7 +50,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ctxIsMergeResultEditor, ctxMergeBaseUri } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; -import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { isWeb } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index 5e094fc4ccc..ae571f8c84d 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -973,7 +973,7 @@ const previousPendingFrame = getPendingFrame(); if (previousPendingFrame) { previousPendingFrame.setAttribute('id', ''); - document.body.removeChild(previousPendingFrame); + previousPendingFrame.remove(); } if (!wasFirstLoad) { pendingMessages = []; @@ -1070,9 +1070,7 @@ if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) { const wasFocused = document.hasFocus(); const oldActiveFrame = getActiveFrame(); - if (oldActiveFrame) { - document.body.removeChild(oldActiveFrame); - } + oldActiveFrame?.remove(); // Styles may have changed since we created the element. Make sure we re-style if (initialStyleVersion !== styleVersion) { applyStyles(newFrame.contentDocument, newFrame.contentDocument.body); diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index fa7b15e39c8..f46e1240428 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,9 @@ + + content="default-src 'none'; script-src 'sha256-ikaxwm2UFoiIKkEZTEU4mnSxpYf3lmsrhy5KqqJZfek=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> + a { + text-decoration: var(--text-link-decoration); + } + a:hover { color: var(--vscode-textLink-activeForeground); } @@ -791,6 +797,17 @@ } } + + function handleInnerDragEvent(/** @type {DragEvent} */ e) { + if (!e.dataTransfer) { + return; + } + + hostMessaging.postMessage('drag', { + shiftKey: e.shiftKey + }); + } + /** * @param {() => void} callback */ @@ -881,7 +898,9 @@ window.addEventListener('keydown', handleInnerKeydown); window.addEventListener('keyup', handleInnerKeyup); window.addEventListener('dragenter', handleInnerDragStartEvent); - window.addEventListener('dragover', handleInnerDragStartEvent); + window.addEventListener('dragover', handleInnerDragEvent); + window.addEventListener('drag', handleInnerDragEvent); + onDomReady(() => { if (!document.body) { @@ -974,7 +993,7 @@ const previousPendingFrame = getPendingFrame(); if (previousPendingFrame) { previousPendingFrame.setAttribute('id', ''); - document.body.removeChild(previousPendingFrame); + previousPendingFrame.remove(); } if (!wasFirstLoad) { pendingMessages = []; @@ -1071,9 +1090,7 @@ if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) { const wasFocused = document.hasFocus(); const oldActiveFrame = getActiveFrame(); - if (oldActiveFrame) { - document.body.removeChild(oldActiveFrame); - } + oldActiveFrame?.remove(); // Styles may have changed since we created the element. Make sure we re-style if (initialStyleVersion !== styleVersion) { applyStyles(newFrame.contentDocument, newFrame.contentDocument.body); @@ -1165,7 +1182,8 @@ }); contentWindow.addEventListener('dragenter', handleInnerDragStartEvent); - contentWindow.addEventListener('dragover', handleInnerDragStartEvent); + contentWindow.addEventListener('dragover', handleInnerDragEvent); + contentWindow.addEventListener('drag', handleInnerDragEvent); unloadMonitor.onIframeLoaded(newFrame); } diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index a6e9943b866..e5fa674ea82 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ // @ts-check -/// /// const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {any} */ (self)); @@ -168,7 +167,7 @@ sw.addEventListener('message', async (event) => { sw.addEventListener('fetch', (event) => { const requestUrl = new URL(event.request.url); - if (requestUrl.protocol === 'https:' && requestUrl.hostname.endsWith('.' + resourceBaseAuthority)) { + if (typeof resourceBaseAuthority === 'string' && requestUrl.protocol === 'https:' && requestUrl.hostname.endsWith('.' + resourceBaseAuthority)) { switch (event.request.method) { case 'GET': case 'HEAD': { diff --git a/src/vs/workbench/contrib/webview/browser/themeing.ts b/src/vs/workbench/contrib/webview/browser/themeing.ts index eda7179665f..75ee4b73061 100644 --- a/src/vs/workbench/contrib/webview/browser/themeing.ts +++ b/src/vs/workbench/contrib/webview/browser/themeing.ts @@ -37,7 +37,7 @@ export class WebviewThemeDataProvider extends Disposable { this._reset(); })); - const webviewConfigurationKeys = ['editor.fontFamily', 'editor.fontWeight', 'editor.fontSize']; + const webviewConfigurationKeys = ['editor.fontFamily', 'editor.fontWeight', 'editor.fontSize', 'accessibility.underlineLinks']; this._register(this._configurationService.onDidChangeConfiguration(e => { if (webviewConfigurationKeys.some(key => e.affectsConfiguration(key))) { this._reset(); @@ -55,6 +55,7 @@ export class WebviewThemeDataProvider extends Disposable { const editorFontFamily = configuration.fontFamily || EDITOR_FONT_DEFAULTS.fontFamily; const editorFontWeight = configuration.fontWeight || EDITOR_FONT_DEFAULTS.fontWeight; const editorFontSize = configuration.fontSize || EDITOR_FONT_DEFAULTS.fontSize; + const linkUnderlines = this._configurationService.getValue('accessibility.underlineLinks'); const theme = this._themeService.getColorTheme(); const exportedColors = colorRegistry.getColorRegistry().getColors().reduce>((colors, entry) => { @@ -72,6 +73,7 @@ export class WebviewThemeDataProvider extends Disposable { 'vscode-editor-font-family': editorFontFamily, 'vscode-editor-font-weight': editorFontWeight, 'vscode-editor-font-size': editorFontSize + 'px', + 'text-link-decoration': linkUnderlines ? 'underline' : 'none', ...exportedColors }; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 3d5d21f080f..8870ddf4cc6 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -34,7 +34,7 @@ import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { areWebviewContentOptionsEqual, IWebview, WebviewContentOptions, WebviewExtensionDescription, WebviewInitInfo, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewFindDelegate, WebviewFindWidget } from 'vs/workbench/contrib/webview/browser/webviewFindWidget'; -import { FromWebviewMessage, KeyEvent, ToWebviewMessage } from 'vs/workbench/contrib/webview/browser/webviewMessages'; +import { FromWebviewMessage, KeyEvent, ToWebviewMessage, WebViewDragEvent } from 'vs/workbench/contrib/webview/browser/webviewMessages'; import { decodeAuthority, webviewGenericCspSource, webviewRootResourceAuthority } from 'vs/workbench/contrib/webview/common/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { CodeWindow } from 'vs/base/browser/window'; @@ -265,6 +265,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD y: elementBox.y + data.clientY }) }); + this._send('set-context-menu-visible', { visible: true }); })); this._register(this.on('load-resource', async (entry) => { @@ -294,7 +295,6 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this._register(Event.runAndSubscribe(webviewThemeDataProvider.onThemeDataChanged, () => this.style())); this._register(_accessibilityService.onDidChangeReducedMotion(() => this.style())); this._register(_accessibilityService.onDidChangeScreenReaderOptimized(() => this.style())); - this._register(contextMenuService.onDidShowContextMenu(() => this._send('set-context-menu-visible', { visible: true }))); this._register(contextMenuService.onDidHideContextMenu(() => this._send('set-context-menu-visible', { visible: false }))); this._confirmBeforeClose = configurationService.getValue('window.confirmBeforeClose'); @@ -310,6 +310,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this._startBlockingIframeDragEvents(); })); + this._register(this.on('drag', (event) => { + this.handleDragEvent('drag', event); + })); + if (initInfo.options.enableFindWidget) { this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this)); } @@ -697,6 +701,17 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this.window?.dispatchEvent(emulatedKeyboardEvent); } + private handleDragEvent(type: 'drag', event: WebViewDragEvent) { + // Create a fake DragEvent from the data provided + const emulatedDragEvent = new DragEvent(type, event); + // Force override the target + Object.defineProperty(emulatedDragEvent, 'target', { + get: () => this.element, + }); + // And re-dispatch + this.window?.dispatchEvent(emulatedDragEvent); + } + windowDidDragStart(): void { // Webview break drag and dropping around the main window (no events are generated when you are over them) // Work around this by disabling pointer events during the drag. diff --git a/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts b/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts index eae9c80fa68..dde553ec057 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts @@ -17,6 +17,10 @@ type KeyEvent = { repeat: boolean; } +type WebViewDragEvent = { + shiftKey: boolean; +} + export type FromWebviewMessage = { 'onmessage': { message: any; transfer?: ArrayBuffer[] }; 'did-click-link': { uri: string }; @@ -36,6 +40,7 @@ export type FromWebviewMessage = { 'did-keyup': KeyEvent; 'did-context-menu': { clientX: number; clientY: number; context: { [key: string]: unknown } }; 'drag-start': void; + 'drag': WebViewDragEvent }; interface UpdateContentEvent { diff --git a/src/vs/workbench/contrib/webview/browser/webviewService.ts b/src/vs/workbench/contrib/webview/browser/webviewService.ts index 9e1d51b89cc..7b75404adf4 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { IOverlayWebview, IWebview, IWebviewElement, IWebviewService, WebviewInitInfo } from 'vs/workbench/contrib/webview/browser/webview'; @@ -58,9 +58,11 @@ export class WebviewService extends Disposable implements IWebviewService { protected registerNewWebview(webview: IWebview) { this._webviews.add(webview); - webview.onDidFocus(() => { + const store = new DisposableStore(); + + store.add(webview.onDidFocus(() => { this._updateActiveWebview(webview); - }); + })); const onBlur = () => { if (this._activeWebview === webview) { @@ -68,10 +70,11 @@ export class WebviewService extends Disposable implements IWebviewService { } }; - webview.onDidBlur(onBlur); - webview.onDidDispose(() => { + store.add(webview.onDidBlur(onBlur)); + store.add(webview.onDidDispose(() => { onBlur(); + store.dispose(); this._webviews.delete(webview); - }); + })); } } diff --git a/src/vs/workbench/contrib/webview/browser/webviewWindowDragMonitor.ts b/src/vs/workbench/contrib/webview/browser/webviewWindowDragMonitor.ts index e4dc5eaf0e9..d009ae186da 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewWindowDragMonitor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewWindowDragMonitor.ts @@ -18,19 +18,41 @@ export class WebviewWindowDragMonitor extends Disposable { constructor(targetWindow: CodeWindow, getWebview: () => IWebview | undefined) { super(); - this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.DRAG_START, () => { + const onDragStart = () => { getWebview()?.windowDidDragStart(); - })); + }; const onDragEnd = () => { getWebview()?.windowDidDragEnd(); }; + this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.DRAG_START, () => { + onDragStart(); + })); + this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.DRAG_END, onDragEnd)); + this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.MOUSE_MOVE, currentEvent => { if (currentEvent.buttons === 0) { onDragEnd(); } })); + + this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.DRAG, (event) => { + if (event.shiftKey) { + onDragEnd(); + } else { + onDragStart(); + } + })); + + this._register(DOM.addDisposableListener(targetWindow, DOM.EventType.DRAG_OVER, (event) => { + if (event.shiftKey) { + onDragEnd(); + } else { + onDragStart(); + } + })); + } } diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts index f24731711d3..3777d363bbf 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -14,34 +14,31 @@ export interface WebviewIcons { readonly dark: URI; } -export class WebviewIconManager implements IDisposable { +export class WebviewIconManager extends Disposable { private readonly _icons = new Map(); private _styleElement: HTMLStyleElement | undefined; - private _styleElementDisposable: DisposableStore | undefined; constructor( @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IConfigurationService private readonly _configService: IConfigurationService, ) { - this._configService.onDidChangeConfiguration(e => { + super(); + this._register(this._configService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('workbench.iconTheme')) { this.updateStyleSheet(); } - }); + })); } - - dispose() { - this._styleElementDisposable?.dispose(); - this._styleElementDisposable = undefined; + override dispose() { + super.dispose(); this._styleElement = undefined; } private get styleElement(): HTMLStyleElement { if (!this._styleElement) { - this._styleElementDisposable = new DisposableStore(); - this._styleElement = dom.createStyleSheet(undefined, undefined, this._styleElementDisposable); + this._styleElement = dom.createStyleSheet(undefined, undefined, this._store); this._styleElement.className = 'webview-icons'; } return this._styleElement; diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts index 1bb57c73a74..6a2dc5881e7 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts @@ -10,7 +10,6 @@ import { isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; import { combinedDisposable, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { EditorActivation } from 'vs/platform/editor/common/editor'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { GroupIdentifier } from 'vs/workbench/common/editor'; @@ -19,7 +18,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IOverlayWebview, IWebviewService, WebviewInitInfo } from 'vs/workbench/contrib/webview/browser/webview'; import { CONTEXT_ACTIVE_WEBVIEW_PANEL_ID } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditor'; import { WebviewIconManager, WebviewIcons } from 'vs/workbench/contrib/webviewPanel/browser/webviewIconManager'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; import { WebviewInput, WebviewInputInitInfo } from './webviewEditorInput'; @@ -213,20 +212,21 @@ export class WebviewEditorService extends Disposable implements IWebviewWorkbenc private readonly _iconManager: WebviewIconManager; - private readonly _activeWebviewPanelIdContext: IContextKey; - constructor( - @IContextKeyService contextKeyService: IContextKeyService, + @IEditorGroupsService editorGroupsService: IEditorGroupsService, @IEditorService private readonly _editorService: IEditorService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IWebviewService private readonly _webviewService: IWebviewService, ) { super(); - this._activeWebviewPanelIdContext = CONTEXT_ACTIVE_WEBVIEW_PANEL_ID.bindTo(contextKeyService); - this._iconManager = this._register(this._instantiationService.createInstance(WebviewIconManager)); + this._register(editorGroupsService.registerContextKeyProvider({ + contextKey: CONTEXT_ACTIVE_WEBVIEW_PANEL_ID, + getGroupContextKeyValue: (group) => this.getWebviewId(group.activeEditor), + })); + this._register(_editorService.onDidActiveEditorChange(() => { this.updateActiveWebview(); })); @@ -248,6 +248,21 @@ export class WebviewEditorService extends Disposable implements IWebviewWorkbenc private readonly _onDidChangeActiveWebviewEditor = this._register(new Emitter()); public readonly onDidChangeActiveWebviewEditor = this._onDidChangeActiveWebviewEditor.event; + private getWebviewId(input: EditorInput | null): string { + let webviewInput: WebviewInput | undefined; + if (input instanceof WebviewInput) { + webviewInput = input; + } else if (input instanceof DiffEditorInput) { + if (input.primary instanceof WebviewInput) { + webviewInput = input.primary; + } else if (input.secondary instanceof WebviewInput) { + webviewInput = input.secondary; + } + } + + return webviewInput?.webview.providedViewType ?? ''; + } + private updateActiveWebview() { const activeInput = this._editorService.activeEditor; @@ -261,13 +276,6 @@ export class WebviewEditorService extends Disposable implements IWebviewWorkbenc newActiveWebview = activeInput.secondary; } } - - if (newActiveWebview) { - this._activeWebviewPanelIdContext.set(newActiveWebview.webview.providedViewType ?? ''); - } else { - this._activeWebviewPanelIdContext.reset(); - } - if (newActiveWebview !== this._activeWebview) { this._activeWebview = newActiveWebview; this._onDidChangeActiveWebviewEditor.fire(newActiveWebview); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 1d374e644c3..664c55b7172 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -69,7 +69,6 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { GettingStartedIndexList } from './gettingStartedList'; -import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; const SLIDE_TRANSITION_TIME_MS = 250; const configurationKey = 'workbench.startupEditor'; @@ -148,7 +147,6 @@ export class GettingStartedPage extends EditorPane { private recentlyOpenedList?: GettingStartedIndexList; private startList?: GettingStartedIndexList; private gettingStartedList?: GettingStartedIndexList; - private videoList?: GettingStartedIndexList; private stepsSlide!: HTMLElement; private categoriesSlide!: HTMLElement; @@ -187,8 +185,7 @@ export class GettingStartedPage extends EditorPane { @IHostService private readonly hostService: IHostService, @IWebviewService private readonly webviewService: IWebviewService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IWorkbenchAssignmentService private readonly tasExperimentService: IWorkbenchAssignmentService + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { super(GettingStartedPage.ID, group, telemetryService, themeService, storageService); @@ -443,10 +440,6 @@ export class GettingStartedPage extends EditorPane { } break; } - case 'hideVideos': { - this.hideVideos(); - break; - } case 'openLink': { this.openerService.open(argument); break; @@ -465,11 +458,6 @@ export class GettingStartedPage extends EditorPane { this.gettingStartedList?.rerender(); } - private hideVideos() { - this.setHiddenCategories([...this.getHiddenCategories().add('getting-started-videos')]); - this.videoList?.setEntries(undefined); - } - private markAllStepsComplete() { if (this.currentWalkthrough) { this.currentWalkthrough?.steps.forEach(step => { @@ -821,29 +809,6 @@ export class GettingStartedPage extends EditorPane { const startList = this.buildStartList(); const recentList = this.buildRecentlyOpenedList(); - - const showVideoTutorials = await Promise.race([ - this.tasExperimentService?.getTreatment('gettingStarted.showVideoTutorials'), - new Promise(resolve => setTimeout(() => resolve(false), 200)) - ]); - - let videoList: GettingStartedIndexList; - if (showVideoTutorials === true) { - this.showFeaturedWalkthrough = false; - videoList = this.buildVideosList(); - const layoutVideos = () => { - if (videoList?.itemCount > 0) { - reset(rightColumn, videoList?.getDomElement(), gettingStartedList.getDomElement()); - } - else { - reset(rightColumn, gettingStartedList.getDomElement()); - } - setTimeout(() => this.categoriesPageScrollbar?.scanDomNode(), 50); - layoutRecentList(); - }; - videoList.onDidChange(layoutVideos); - } - const gettingStartedList = this.buildGettingStartedWalkthroughsList(); const footer = $('.footer', {}, @@ -855,31 +820,18 @@ export class GettingStartedPage extends EditorPane { const layoutLists = () => { if (gettingStartedList.itemCount) { this.container.classList.remove('noWalkthroughs'); - if (videoList?.itemCount > 0) { - this.container.classList.remove('noVideos'); - reset(rightColumn, videoList?.getDomElement(), gettingStartedList.getDomElement()); - } else { - this.container.classList.add('noVideos'); - reset(rightColumn, gettingStartedList.getDomElement()); - } + reset(rightColumn, gettingStartedList.getDomElement()); } else { this.container.classList.add('noWalkthroughs'); - if (videoList?.itemCount > 0) { - this.container.classList.remove('noVideos'); - reset(rightColumn, videoList?.getDomElement()); - } - else { - this.container.classList.add('noVideos'); - reset(rightColumn); - } + reset(rightColumn); } setTimeout(() => this.categoriesPageScrollbar?.scanDomNode(), 50); layoutRecentList(); }; const layoutRecentList = () => { - if (this.container.classList.contains('noWalkthroughs') && this.container.classList.contains('noVideos')) { + if (this.container.classList.contains('noWalkthroughs')) { recentList.setLimit(10); reset(leftColumn, startList.getDomElement()); reset(rightColumn, recentList.getDomElement()); @@ -1139,69 +1091,6 @@ export class GettingStartedPage extends EditorPane { return gettingStartedList; } - private buildVideosList(): GettingStartedIndexList { - - const renderFeaturedExtensions = (entry: IWelcomePageStartEntry): HTMLElement => { - - const featuredBadge = $('.featured-badge', {}); - const descriptionContent = $('.description-content', {},); - - reset(featuredBadge, $('.featured', {}, $('span.featured-icon.codicon.codicon-star-full'))); - reset(descriptionContent, ...renderLabelWithIcons(entry.description)); - - const titleContent = $('h3.category-title.max-lines-3', { 'x-category-title-for': entry.id }); - reset(titleContent, ...renderLabelWithIcons(entry.title)); - - return $('button.getting-started-category' + '.featured', - { - 'x-dispatch': 'openLink:' + entry.command, - 'title': entry.title - }, - featuredBadge, - $('.main-content', {}, - this.iconWidgetFor(entry), - titleContent, - $('a.codicon.codicon-close.hide-category-button', { - 'tabindex': 0, - 'x-dispatch': 'hideVideos', - 'title': localize('close', "Hide"), - 'role': 'button', - 'aria-label': localize('closeAriaLabel', "Hide"), - }), - ), - descriptionContent); - }; - - if (this.videoList) { - this.videoList.dispose(); - } - const videoList = this.videoList = new GettingStartedIndexList( - { - title: localize('videos', "Videos"), - klass: 'getting-started-videos', - limit: 1, - renderElement: renderFeaturedExtensions, - contextService: this.contextService, - }); - - if (this.getHiddenCategories().has('getting-started-videos')) { - return videoList; - } - - videoList.setEntries([{ - id: 'getting-started-videos', - title: localize('videos-title', 'Watch Getting Started Tutorials'), - description: localize('videos-description', 'Learn VS Code\'s must-have features in short and practical videos'), - command: 'https://aka.ms/vscode-getting-started-tutorials', - order: 0, - icon: { type: 'icon', icon: Codicon.deviceCameraVideo }, - when: ContextKeyExpr.true(), - }]); - videoList.onDidChange(() => this.registerDispatchListeners()); - - return videoList; - } - layout(size: Dimension) { this.detailsScrollbar?.scanDomNode(); @@ -1211,7 +1100,6 @@ export class GettingStartedPage extends EditorPane { this.startList?.layout(size); this.gettingStartedList?.layout(size); this.recentlyOpenedList?.layout(size); - this.videoList?.layout(size); if (this.editorInput?.selectedStep && this.currentMediaType) { this.mediaDisposables.clear(); @@ -1378,7 +1266,7 @@ export class GettingStartedPage extends EditorPane { } private buildMarkdownDescription(container: HTMLElement, text: LinkedText[]) { - while (container.firstChild) { container.removeChild(container.firstChild); } + while (container.firstChild) { container.firstChild.remove(); } for (const linkedText of text) { if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts index 8e934b891a3..e3227a80af4 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts @@ -7,14 +7,14 @@ import { darken, inputBackground, editorWidgetBackground, lighten, registerColor import { localize } from 'vs/nls'; // Seprate from main module to break dependency cycles between welcomePage and gettingStarted. -export const welcomePageBackground = registerColor('welcomePage.background', { light: null, dark: null, hcDark: null, hcLight: null }, localize('welcomePage.background', 'Background color for the Welcome page.')); +export const welcomePageBackground = registerColor('welcomePage.background', null, localize('welcomePage.background', 'Background color for the Welcome page.')); export const welcomePageTileBackground = registerColor('welcomePage.tileBackground', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: '#000', hcLight: editorWidgetBackground }, localize('welcomePage.tileBackground', 'Background color for the tiles on the Welcome page.')); export const welcomePageTileHoverBackground = registerColor('welcomePage.tileHoverBackground', { dark: lighten(editorWidgetBackground, .2), light: darken(editorWidgetBackground, .1), hcDark: null, hcLight: null }, localize('welcomePage.tileHoverBackground', 'Hover background color for the tiles on the Welcome.')); export const welcomePageTileBorder = registerColor('welcomePage.tileBorder', { dark: '#ffffff1a', light: '#0000001a', hcDark: contrastBorder, hcLight: contrastBorder }, localize('welcomePage.tileBorder', 'Border color for the tiles on the Welcome page.')); -export const welcomePageProgressBackground = registerColor('welcomePage.progress.background', { light: inputBackground, dark: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('welcomePage.progress.background', 'Foreground color for the Welcome page progress bars.')); -export const welcomePageProgressForeground = registerColor('welcomePage.progress.foreground', { light: textLinkForeground, dark: textLinkForeground, hcDark: textLinkForeground, hcLight: textLinkForeground }, localize('welcomePage.progress.foreground', 'Background color for the Welcome page progress bars.')); +export const welcomePageProgressBackground = registerColor('welcomePage.progress.background', inputBackground, localize('welcomePage.progress.background', 'Foreground color for the Welcome page progress bars.')); +export const welcomePageProgressForeground = registerColor('welcomePage.progress.foreground', textLinkForeground, localize('welcomePage.progress.foreground', 'Background color for the Welcome page progress bars.')); export const walkthroughStepTitleForeground = registerColor('walkthrough.stepTitle.foreground', { light: '#000000', dark: '#ffffff', hcDark: null, hcLight: null }, localize('walkthrough.stepTitle.foreground', 'Foreground color of the heading of each walkthrough step')); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts index 869f44526b4..fef4b4928bf 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -211,7 +211,7 @@ export class GettingStartedDetailsRenderer { private async readAndCacheStepMarkdown(path: URI, base: URI): Promise { if (!this.mdCache.has(path)) { const contents = await this.readContentsOfPath(path); - const markdownContents = await renderMarkdownDocument(transformUris(contents, base), this.extensionService, this.languageService, true, true); + const markdownContents = await renderMarkdownDocument(transformUris(contents, base), this.extensionService, this.languageService, { allowUnknownProtocols: true }); this.mdCache.set(path, markdownContents); } return assertIsDefined(this.mdCache.get(path)); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts index 50144d87e1c..caab78fd561 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts @@ -123,7 +123,7 @@ export class GettingStartedIndexList .content .gettingStartedContainer .icon-widget, -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories .icon-widget:not(.codicon-device-camera-video), .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories .featured-icon { font-size: 20px; padding-right: 8px; @@ -236,13 +235,6 @@ top: 3px; } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories .icon-widget.codicon-device-camera-video { - font-size: 20px; - padding-right: 8px; - position: relative; - transform: translateY(+100%); -} - .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories .codicon:not(.icon-widget, .featured-icon, .hide-category-button) { margin: 0 2px; } @@ -348,7 +340,7 @@ right: 8px; } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category.featured .icon-widget:not(.codicon-device-camera-video) { +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category.featured .icon-widget { visibility: hidden; } @@ -931,6 +923,7 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer .button-link { color: var(--vscode-textLink-foreground); + text-decoration: var(--text-link-decoration); } .monaco-workbench .part.editor > .content .gettingStartedContainer .button-link .codicon { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts b/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts index b7b09ca9417..3a63341f1d8 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { FileAccess } from 'vs/base/common/network'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { LanguageService } from 'vs/editor/common/services/languageService'; diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css b/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css index ed362ff32c5..7ab127eaab4 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css @@ -18,7 +18,7 @@ } .monaco-workbench .part.editor > .content .walkThroughContent a { - text-decoration: none; + text-decoration: var(--text-link-decoration); } .monaco-workbench .part.editor > .content .walkThroughContent a:focus, @@ -153,6 +153,8 @@ .monaco-workbench .part.editor > .content .walkThroughContent code, .monaco-workbench .part.editor > .content .walkThroughContent .shortcut { color: var(--vscode-textPreformat-foreground); + background-color: var(--vscode-textPreformat-background); + border-radius: 3px; } .monaco-workbench .part.editor > .content .walkThroughContent .monaco-editor { diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts index dd11b080f6b..34efe4d154e 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts @@ -32,7 +32,7 @@ import { UILabelProvider } from 'vs/base/common/keybindingLabels'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { deepClone } from 'vs/base/common/objects'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { addDisposableListener, Dimension, safeInnerHtml, size } from 'vs/base/browser/dom'; +import { addDisposableListener, Dimension, isHTMLAnchorElement, isHTMLButtonElement, isHTMLElement, safeInnerHtml, size } from 'vs/base/browser/dom'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -142,12 +142,12 @@ export class WalkThroughPart extends EditorPane { })); this.disposables.add(this.addEventListener(this.content, 'focusin', (e: FocusEvent) => { // Work around scrolling as side-effect of setting focus on the offscreen zone widget (#18929) - if (e.target instanceof HTMLElement && e.target.classList.contains('zone-widget-container')) { + if (isHTMLElement(e.target) && e.target.classList.contains('zone-widget-container')) { const scrollPosition = this.scrollbar.getScrollPosition(); this.content.scrollTop = scrollPosition.scrollTop; this.content.scrollLeft = scrollPosition.scrollLeft; } - if (e.target instanceof HTMLElement) { + if (isHTMLElement(e.target)) { this.lastFocus = e.target; } })); @@ -156,7 +156,7 @@ export class WalkThroughPart extends EditorPane { private registerClickHandler() { this.content.addEventListener('click', event => { for (let node = event.target as HTMLElement; node; node = node.parentNode as HTMLElement) { - if (node instanceof HTMLAnchorElement && node.href) { + if (isHTMLAnchorElement(node) && node.href) { const baseElement = node.ownerDocument.getElementsByTagName('base')[0] || this.window.location; if (baseElement && node.href.indexOf(baseElement.href) >= 0 && node.hash) { const scrollTarget = this.content.querySelector(node.hash); @@ -171,7 +171,7 @@ export class WalkThroughPart extends EditorPane { } event.preventDefault(); break; - } else if (node instanceof HTMLButtonElement) { + } else if (isHTMLButtonElement(node)) { const href = node.getAttribute('data-href'); if (href) { this.open(URI.parse(href)); @@ -414,7 +414,7 @@ export class WalkThroughPart extends EditorPane { const keybinding = command && this.keybindingService.lookupKeybinding(command); const label = keybinding ? keybinding.getLabel() || '' : UNBOUND_COMMAND; while (key.firstChild) { - key.removeChild(key.firstChild); + key.firstChild.remove(); } key.appendChild(document.createTextNode(label)); }); @@ -433,7 +433,7 @@ export class WalkThroughPart extends EditorPane { const keys = this.content.querySelectorAll('.multi-cursor-modifier'); Array.prototype.forEach.call(keys, (key: Element) => { while (key.firstChild) { - key.removeChild(key.firstChild); + key.firstChild.remove(); } key.appendChild(document.createTextNode(modifier)); }); diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 2115ddfecc0..4ae3a9f94ed 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -172,15 +172,15 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sand 'type': 'string', 'enum': ['preserve', 'all', 'folders', 'one', 'none'], 'enumDescriptions': [ - localize('window.reopenFolders.preserve', "Always reopen all windows. If a folder or workspace is opened (e.g. from the command line) it opens as a new window unless it was opened before. If files are opened they will open in one of the restored windows."), - localize('window.reopenFolders.all', "Reopen all windows unless a folder, workspace or file is opened (e.g. from the command line)."), - localize('window.reopenFolders.folders', "Reopen all windows that had folders or workspaces opened unless a folder, workspace or file is opened (e.g. from the command line)."), - localize('window.reopenFolders.one', "Reopen the last active window unless a folder, workspace or file is opened (e.g. from the command line)."), + localize('window.reopenFolders.preserve', "Always reopen all windows. If a folder or workspace is opened (e.g. from the command line) it opens as a new window unless it was opened before. If files are opened they will open in one of the restored windows together with editors that were previously opened."), + localize('window.reopenFolders.all', "Reopen all windows unless a folder, workspace or file is opened (e.g. from the command line). If a file is opened, it will replace any of the editors that were previously opened in a window."), + localize('window.reopenFolders.folders', "Reopen all windows that had folders or workspaces opened unless a folder, workspace or file is opened (e.g. from the command line). If a file is opened, it will replace any of the editors that were previously opened in a window."), + localize('window.reopenFolders.one', "Reopen the last active window unless a folder, workspace or file is opened (e.g. from the command line). If a file is opened, it will replace any of the editors that were previously opened in a window."), localize('window.reopenFolders.none', "Never reopen a window. Unless a folder or workspace is opened (e.g. from the command line), an empty window will appear.") ], 'default': 'all', 'scope': ConfigurationScope.APPLICATION, - 'description': localize('restoreWindows', "Controls how windows are being reopened after starting for the first time. This setting has no effect when the application is already running.") + 'description': localize('restoreWindows', "Controls how windows and editors within are being restored when opening.") }, 'window.restoreFullscreen': { 'type': 'boolean', @@ -226,7 +226,7 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sand 'type': 'boolean', 'default': false, 'scope': ConfigurationScope.APPLICATION, - 'markdownDescription': localize('window.doubleClickIconToClose', "If enabled, this setting will close the window when the application icon in the title bar is double-clicked. The window will not be able to be dragged by the icon. This setting is effective only if `#window.titleBarStyle#` is set to `custom`.") + 'markdownDescription': localize('window.doubleClickIconToClose', "If enabled, this setting will close the window when the application icon in the title bar is double-clicked. The window will not be able to be dragged by the icon. This setting is effective only if {0} is set to `custom`.", '`#window.titleBarStyle#`') }, 'window.titleBarStyle': { 'type': 'string', @@ -241,11 +241,11 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sand 'markdownEnumDescriptions': [ localize(`window.customTitleBarVisibility.auto`, "Automatically changes custom title bar visibility."), localize(`window.customTitleBarVisibility.windowed`, "Hide custom titlebar in full screen. When not in full screen, automatically change custom title bar visibility."), - localize(`window.customTitleBarVisibility.never`, "Hide custom titlebar when `#window.titleBarStyle#` is set to `native`."), + localize(`window.customTitleBarVisibility.never`, "Hide custom titlebar when {0} is set to `native`.", '`#window.titleBarStyle#`'), ], 'default': isLinux ? 'never' : 'auto', 'scope': ConfigurationScope.APPLICATION, - 'markdownDescription': localize('window.customTitleBarVisibility', "Adjust when the custom title bar should be shown. The custom title bar can be hidden when in full screen mode with `windowed`. The custom title bar can only be hidden in none full screen mode with `never` when `#window.titleBarStyle#` is set to `native`."), + 'markdownDescription': localize('window.customTitleBarVisibility', "Adjust when the custom title bar should be shown. The custom title bar can be hidden when in full screen mode with `windowed`. The custom title bar can only be hidden in none full screen mode with `never` when {0} is set to `native`.", '`#window.titleBarStyle#`'), }, 'window.dialogStyle': { 'type': 'string', @@ -360,6 +360,10 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sand type: 'boolean', description: localize('argv.disableLcdText', 'Disables LCD font antialiasing.') }, + 'proxy-bypass-list': { + type: 'string', + description: localize('argv.proxyBypassList', 'Bypass any specified proxy for the given semi-colon-separated list of hosts. Example value ";*.microsoft.com;*foo.com;1.2.3.4:5678", will use the proxy server for all hosts except for local addresses (localhost, 127.0.0.1 etc.), microsoft.com subdomains, hosts that contain the suffix foo.com and anything at 1.2.3.4:5678') + }, 'disable-hardware-acceleration': { type: 'boolean', description: localize('argv.disableHardwareAcceleration', 'Disables hardware acceleration. ONLY change this option if you encounter graphic issues.') diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 73ebdb1b2a9..140e12ee47d 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -204,7 +204,10 @@ export class NativeWindow extends BaseWindow { [{ label: localize('restart', "Restart"), run: () => this.nativeHostService.relaunch() - }] + }], + { + priority: NotificationPriority.URGENT + } ); }); @@ -248,7 +251,7 @@ export class NativeWindow extends BaseWindow { ); }); - ipcRenderer.on('vscode:showTranslatedBuildWarning', (event: unknown, message: string) => { + ipcRenderer.on('vscode:showTranslatedBuildWarning', () => { this.notificationService.prompt( Severity.Warning, localize("runningTranslated", "You are running an emulated version of {0}. For better performance download the native arm64 version of {0} build for your machine.", this.productService.nameLong), @@ -260,7 +263,24 @@ export class NativeWindow extends BaseWindow { const insidersURL = 'https://code.visualstudio.com/docs/?dv=osx&build=insiders'; this.openerService.open(quality === 'stable' ? stableURL : insidersURL); } - }] + }], + { + priority: NotificationPriority.URGENT + } + ); + }); + + ipcRenderer.on('vscode:showArgvParseWarning', (event: unknown, message: string) => { + this.notificationService.prompt( + Severity.Warning, + localize("showArgvParseWarning", "The runtime arguments file 'argv.json' contains errors. Please correct them and restart."), + [{ + label: localize('showArgvParseWarningAction', "Open File"), + run: () => this.editorService.openEditor({ resource: this.nativeEnvironmentService.argvResource }) + }], + { + priority: NotificationPriority.URGENT + } ); }); @@ -874,7 +894,7 @@ export class NativeWindow extends BaseWindow { // Handle external open() calls this.openerService.setDefaultExternalOpener({ openExternal: async (href: string) => { - const success = await this.nativeHostService.openExternal(href); + const success = await this.nativeHostService.openExternal(href, this.configurationService.getValue('workbench.externalBrowser')); if (!success) { const fileCandidate = URI.parse(href); if (fileCandidate.scheme === Schemas.file) { diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 6791e00042c..9e34f460806 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -15,7 +15,6 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { index } from 'vs/base/common/arrays'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; -import { ApiProposalName } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData, Extensions as ExtensionFeaturesExtensions } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { IExtensionManifest, IKeyBinding } from 'vs/platform/extensions/common/extensions'; @@ -25,6 +24,7 @@ import { platform } from 'vs/base/common/process'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ApiProposalName } from 'vs/platform/extensions/common/extensionsApiProposals'; interface IAPIMenu { readonly key: string; @@ -343,6 +343,11 @@ const apiMenus: IAPIMenu[] = [ id: MenuId.TestItemGutter, description: localize('testing.item.gutter.title', "The menu for a gutter decoration for a test item"), }, + { + key: 'testing/profiles/context', + id: MenuId.TestProfilesContext, + description: localize('testing.profiles.context.title', "The menu for configuring testing profiles."), + }, { key: 'testing/item/result', id: MenuId.TestPeekElement, diff --git a/src/vs/workbench/services/aiRelatedInformation/test/common/aiRelatedInformationService.test.ts b/src/vs/workbench/services/aiRelatedInformation/test/common/aiRelatedInformationService.test.ts index 52e00d57f7b..db9b4febfbd 100644 --- a/src/vs/workbench/services/aiRelatedInformation/test/common/aiRelatedInformationService.test.ts +++ b/src/vs/workbench/services/aiRelatedInformation/test/common/aiRelatedInformationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { AiRelatedInformationService } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformationService'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 2509a589d38..d154337b9a5 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableMap, DisposableStore, IDisposable, isDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, isDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { isString } from 'vs/base/common/types'; import { localize } from 'vs/nls'; @@ -12,7 +12,7 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { IProductService } from 'vs/platform/product/common/productService'; import { ISecretStorageService } from 'vs/platform/secrets/common/secrets'; import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; -import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -164,10 +164,10 @@ export class AuthenticationService extends Disposable implements IAuthentication throw new Error(`No authentication provider '${id}' is currently registered.`); } - async getSessions(id: string, scopes?: string[], activateImmediate: boolean = false): Promise> { + async getSessions(id: string, scopes?: string[], account?: AuthenticationSessionAccount, activateImmediate: boolean = false): Promise> { const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate); if (authProvider) { - return await authProvider.getSessions(scopes); + return await authProvider.getSessions(scopes, { account }); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); } @@ -177,7 +177,7 @@ export class AuthenticationService extends Disposable implements IAuthentication const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate); if (authProvider) { return await authProvider.createSession(scopes, { - sessionToRecreate: options?.sessionToRecreate + account: options?.account }); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); @@ -200,10 +200,12 @@ export class AuthenticationService extends Disposable implements IAuthentication return provider; } + const store = new DisposableStore(); + // When activate has completed, the extension has made the call to `registerAuthenticationProvider`. // However, activate cannot block on this, so the renderer may not have gotten the event yet. const didRegister: Promise = new Promise((resolve, _) => { - this.onDidRegisterAuthenticationProvider(e => { + store.add(Event.once(this.onDidRegisterAuthenticationProvider)(e => { if (e.id === providerId) { provider = this._authenticationProviders.get(providerId); if (provider) { @@ -212,16 +214,18 @@ export class AuthenticationService extends Disposable implements IAuthentication throw new Error(`No authentication provider '${providerId}' is currently registered.`); } } - }); + })); }); const didTimeout: Promise = new Promise((_, reject) => { - setTimeout(() => { + const handle = setTimeout(() => { reject('Timed out waiting for authentication provider to register'); }, 5000); + + store.add(toDisposable(() => clearTimeout(handle))); }); - return Promise.race([didRegister, didTimeout]); + return Promise.race([didRegister, didTimeout]).finally(() => store.dispose()); } } diff --git a/src/vs/workbench/services/authentication/common/authentication.ts b/src/vs/workbench/services/authentication/common/authentication.ts index 6da6e530237..1d99ffc059a 100644 --- a/src/vs/workbench/services/authentication/common/authentication.ts +++ b/src/vs/workbench/services/authentication/common/authentication.ts @@ -35,8 +35,12 @@ export interface AuthenticationProviderInformation { } export interface IAuthenticationCreateSessionOptions { - sessionToRecreate?: AuthenticationSession; activateImmediate?: boolean; + /** + * The account that is being asked about. If this is passed in, the provider should + * attempt to return the sessions that are only related to this account. + */ + account?: AuthenticationSessionAccount; } export interface AllowedExtension { @@ -131,7 +135,7 @@ export interface IAuthenticationService { * @param scopes The scopes for the session * @param activateImmediate If true, the provider should activate immediately if it is not already */ - getSessions(id: string, scopes?: string[], activateImmediate?: boolean): Promise>; + getSessions(id: string, scopes?: string[], account?: AuthenticationSessionAccount, activateImmediate?: boolean): Promise>; /** * Creates an AuthenticationSession with the given provider and scopes @@ -162,8 +166,12 @@ export interface IAuthenticationExtensionsService { requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; } -export interface IAuthenticationProviderCreateSessionOptions { - sessionToRecreate?: AuthenticationSession; +export interface IAuthenticationProviderSessionOptions { + /** + * The account that is being asked about. If this is passed in, the provider should + * attempt to return the sessions that are only related to this account. + */ + account?: AuthenticationSessionAccount; } /** @@ -194,9 +202,10 @@ export interface IAuthenticationProvider { /** * Retrieves a list of authentication sessions. * @param scopes - An optional list of scopes. If provided, the sessions returned should match these permissions, otherwise all sessions should be returned. + * @param options - Additional options for getting sessions. * @returns A promise that resolves to an array of authentication sessions. */ - getSessions(scopes?: string[]): Promise; + getSessions(scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions): Promise; /** * Prompts the user to log in. @@ -207,7 +216,7 @@ export interface IAuthenticationProvider { * @param options - Additional options for creating the session. * @returns A promise that resolves to an authentication session. */ - createSession(scopes: string[], options: IAuthenticationProviderCreateSessionOptions): Promise; + createSession(scopes: string[], options: IAuthenticationProviderSessionOptions): Promise; /** * Removes the session corresponding to the specified session ID. diff --git a/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts index 854aea75cda..891c718ac73 100644 --- a/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts +++ b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { AuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index e47c8923019..5178f4c9457 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -6,14 +6,14 @@ import { localize } from 'vs/nls'; import { mark } from 'vs/base/common/performance'; import { Emitter, Event } from 'vs/base/common/event'; -import { Dimension, EventHelper, EventType, ModifierKeyEmitter, addDisposableListener, cloneGlobalStylesheets, copyAttributes, createLinkElement, createMetaElement, getActiveWindow, getClientArea, getWindowId, isGlobalStylesheet, position, registerWindow, sharedMutationObserver, trackAttributes } from 'vs/base/browser/dom'; +import { Dimension, EventHelper, EventType, ModifierKeyEmitter, addDisposableListener, cloneGlobalStylesheets, copyAttributes, createLinkElement, createMetaElement, getActiveWindow, getClientArea, getWindowId, isGlobalStylesheet, isHTMLElement, position, registerWindow, sharedMutationObserver, trackAttributes } from 'vs/base/browser/dom'; import { CodeWindow, ensureCodeWindow, mainWindow } from 'vs/base/browser/window'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { isWeb } from 'vs/base/common/platform'; +import { isFirefox, isWeb } from 'vs/base/common/platform'; import { IRectangle, WindowMinimumSize } from 'vs/platform/window/common/window'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; @@ -332,7 +332,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili options?.mode === AuxiliaryWindowMode.Fullscreen ? 'window-fullscreen=yes' : undefined // non-standard property ]); - const auxiliaryWindow = mainWindow.open('about:blank', undefined, features.join(',')); + const auxiliaryWindow = mainWindow.open(isFirefox ? '' /* FF immediately fires an unload event if using about:blank */ : 'about:blank', undefined, features.join(',')); if (!auxiliaryWindow && isWeb) { return (await this.dialogService.prompt({ type: Severity.Warning, @@ -461,7 +461,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili for (const node of mutation.addedNodes) { //